Redis数据结构及应用场景

本文介绍了Redis中的数据结构,包括字符串、散列、列表、集合和有序集合,以及它们在实际应用中的场景,如存储文章标签、限流控制、分页等。字符串和散列适合存储对象和映射关系,列表可用于分页和记录历史,集合用于存储唯一元素,有序集合则支持按分数排序。此外,文中还探讨了Redis在限流场景中的应用,如过期时间和队列限流策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

结绳记事,总结,思考,才能有所成长~

1.字符串类型

字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。一个字符串类型键允许存储的数据的最大容量是512MB。
字符串类型是其他4中数据类型的基础,其他数据类型和字符串的差别从某种程度上来说只是组织字符串的形式不同。例如,列表类型是以列表形式组织字符串,而集合类型是以集合的形式组织字符串。

2.散列(hash table)类型

散列类型适合存储对象:使用对象类别和ID构成键名,使用字段表示对象的属性,而字段值则存储属性值

类似于存储的是关系型数据库中的一条记录。这种结构也可以作为存储映射关系的集合,比如使用散列类型的键slug.to.id来存储文章略缩名和ID之间的映射关系。其中字段用来记录略缩名,字段值用来记录略缩名对应的文章ID

常见命令如下:

HSET key field value
> HSET car price 500
HGET key field
HMSET key field value [field value……]
HMGET key field [field……]
HGETALL key

HSET命令的方便之处在于不区分插入和更新操作,这意味着修改数据时不用事先判断字段是否存在来决定要执行的是插入操作还是更新操作

3.列表类型

列表类型(list)可以存储一个有序的字符串列表,内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的(和从只有20个元素的列表中获取头部或尾部的10条记录的速度是一样的)。
不过使用链表的代价是通过索引访问元素比较慢。

使用的场景:

简单场景分页:分页的缺点:列表中间增加、删除、修改元素比较繁琐,因为涉及到重新排列;另外当列表元素较多时,访问中间的页面性能较差:列表类型是通过链表实现的,所以它的索引效率并不高。可以使用有序集合类型来分页。

# 向列表左/右边增加元素,返回值表示增加元素后列表的长度
LPUSH key value [value……]
RPUSH key value [value……]
# 获取列表片段,分页,如果起止位置为负数,表示从右边开始计算序号,如“-1”表示最右边第一个元素,“-2”表示最右边第二个元素
LRANGE key start stop
> LRANGE numbers 0 2

保留最新的N条记录(获取列表的某一个片段)
LTRIMLPUSH命令一起使用来限制列表中元素的数量,比如记录日志时我们希望只保留最近的100条日志,则每次加入新元素时调用一次LTRIM命令即可。

LPUSH logs $newLog
LTRIM logs 0 99

# LTRIM命令可以删除指定索引范围之外的所有元素
LTRIM key start end

网站监控系统:当把列表类型作为队列使用时,RPOPLPUSH命令可以很直观地在多个队列中传递数据。当source和destination相同时,则会不断地将队尾元素移动到队首,借助这个特性可以实现一个网站监控系统,使用一个队列存储需要监控的网址,然后监控程序不断地使用RPOPLPUSH命令循环取出一个网址来测试可用性。好处在于在 程序执行过程中仍然可以不断地向网址列表中加入新网址。

# 向列表中添加元素(从左到右)
LPUSH key value [value……]
# 获取指定位置的元素(从左到右,如果index为负数,表示从右到左)
LINDEX key index
# 获取列表长度
LLEN key
# 只保留列表指定片段,删除指定索引范围外的所有元素,适用于新鲜事、热门事件等场景
LTRIM key start end
# 从source列表类型键的右边弹出一个元素,然后将其加入到destination类型键的左边,并返回这个元素的值,整个过程是原子的。
RPOPLPUSH source destination

4.集合类型

集合类型的常用操作是向集合中加入、删除元素、判断某个元素是否存在等,由于集合类型在Redis内部是使用值为空的散列表(hash table)实现的,所以这些操作的时间复杂度都是O(1)。最方便的是多个集合之间还可以进行交集,并集,差集的操作

常用命令

# 添加/删除元素
SADD key member [member ……]
SREM key member [member……]
# 获得集合中所有元素
SMEMBERS key
# 元素是否在集合中
SISMEMBER key
# 集合中元素个数
SCARD key
# 随机获得集合中的元素
SRANDMEMBER key
# 随机移除一个元素,可用于抢券场景
SPOP key
# 差集
SDIFF key [key……]
# 交集
SINTER key [key……]
# 并集
SUNION key [key……]

使用场景

存储文章标签

有时需要列出某个标签下的所有文章,甚至需要获得同时属于几个标签的文章列表。常规的数据库方式我们可能要使用3张表来存储这些关系(文章表、标签表、文章标签关系表),并通过复杂的SQL来获取想要的结果,但通过集合,我们可以方便高效的实现。

比如要找到同时属于“java”、“MySQL”、“Redis”这3个标签的文章,可以使用tag:标签名称:posts的集合类型存储标有该标签的文章ID集合,通过tag:MySQL:posts即可轻松获取MySQL标签下的文章,通过SINTER命令可获得同时属于多个标签的文章

抢券

通过集合的SPOP命令,随机从券码池中获取元素,当获取的元素为空时,说明券码已经分配完了。

5.有序集合类型(sorted set)

在集合类型的基础上有序集合类型为集合中的每个元素都关联了一个分数,使得我们不仅可以进行插入、删除和判断元素是否存在等集合类型支持的操作,还能够获得分数最高(或最低)的前N个元素,获得指定范围的元素等与分数有关的操作

常用命令

# 添加元素,像有序集合中增加一个元素和该元素的分数,如果该元素已经存在,则只更新分数
ZADD key score member [score member……]
# 增加某个元素的分数,inccrement为增加的分数,member为指定的元素
ZINCRBY key increment member
# 获取某个元素的排名
ZSCORE key member
# 获得排名在某个范围的列表(从小到大),排名榜场景
ZRANGE key start stop [WITHSCORES]
# 倒序排,从大到小
ZREVRANGE key start stop [WITHSCORES]
# 获得指定分数范围的元素
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

使用场景

文章按点击量排名

要按照文章的点击量排序,就必须再额外使用一个有序集合类型的键来实现。在这个键中已文章的ID作为元素,以文章的点击量作为该元素的分数。将该键命名为posts:page.view,每次用户访问一篇文章时,就通过ZINCRBY posts:page.view 1 文章ID更新访问量。想要获得文章的访问量,通过ZSCORE posts:page.view 文章ID来实现。

按照时间排序

将时间转为时间戳,创建post:create.time的有序集合key,以文章ID为元素,创建的时间戳为得分,可进行排名。通过修改时间即可以达到修改排名的目的。还可以通过ZREVRANGEBYSCORE命令还可以轻松获得指定时间范围的文章列表

6.Redis擅长的场景

过期时间限流

比如为了防止某个IP或某个用户恶意攻击服务,可以通过Redis的key的过期特性,实现访问频率的限制,思路是设置rate.limiting:用户IP的字符串类型键,1min后过期,每次用户访问时使用INCR命令递增该键的值,当值大于指定阈值后,拒绝访问,伪代码如下:

	# 请求打到后端服务
	# INCR返回加1后的值,key不存在时默认返回1
	long visitCount = INCR rate.limiting:用户IP
	# 首次访问,设置过期时间,同时可能存在只设置了key,还没设置过期时间服务器就重启的情况,所以要判断TTL
	if (visitCount == 1 || ttl == null) {
		setEx rate.limiting:用户IP 60
	} else {
		if(visitCount > threshold) {
			# 拒绝访问
			return
		}
	}

缺点:如果请求并发量大,仍有可能绕过该限制,比如限制100次/min请求,如果仅在第1min的后30s有99次请求,在第2min的前30s后99次请求,并不能达到限制访问频率的目的,如何改进,可考虑队列限流。

队列限流

对每个IP或用户,使用一个列表类型的键来记录他最近N次的访问时间戳,当N等于阈值threshold时,取出队列中的第一个元素,即首次访问的时间,跟当前时间对比,来判断是否小于1min,伪代码如下:

# 请求打到后端服务
# 将请求时间戳放入队列
LPUSH rate.limiting:用户IP now().timestamp
long visitCount = LLEN rate.limiting:用户IP
# 超过阈值
if (visitCount > threshold) {
    # 获取首次访问的时间戳
	long first = LINDEX rate.limiting:用户IP -1
	# 保留最近threshold条记录
	LTRIM rate.limiting:用户IP 0, -2
	# 超过100次/min的限制
	if (now.timestamp - first < 60) {
		return;
	}
}

缺点:如果阈值threshold比较大,则会占用比较多的存储空间。

Guava的本地限流

可参考令牌桶算法、漏桶算法。
优点:集群环境中,可保证每个节点都不会被打挂,因为是本地限流。
缺点:不适合按总量限流的场景,比如总共要发放1w张优惠券,有2台服务器,并不能为每台服务器设置5000张的阈值,因为通过负载均衡是无法做到真正的均衡的,设置的过多,会导致优惠券的超卖,设置的过少,会导致优惠券的少买。
用来按频率限制的话,一定要考虑限制的频率和集群中节点数量的关系。比如限制每个用户1w/min请求,有2台服务器,则每台服务器按照≈5000次/min的频次设置。具体要看负载均衡策略了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值