基础:万丈高楼平地起--Redis基础数据结构
Redis的字符串是动态字符串SDS,采用预分配冗余空间的方式来减少内存的频繁分配。
在字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会增加1M。
字符串最大长度为512M
list(列表)底层存储在元素较少的情况下,使用一块连续内存存储,数据结构使用ziplist(压缩列表),数据量比较多的时候才会改成quicklist
quicklist将链表和ziplist结合起来,将多个ziplist使用双向指针串起来使用
redis为了高性能,不阻塞服务,采用渐进式rehash策略。查询时会同时查询两个hash结构,在后续的定时任务、hash的子指令中,循序渐进地将旧hash内容一点点迁移到新的hash结构中
zset在内部实现中采用了跳跃表的数据结构
list/set/hash/zset共享两条通用规则:create if not exists,drop if no elements
应用1:千帆竞发--分布式锁
redis实现分布式锁使用setnx(set if not exists)指令
为了避免程序异常造成死锁,通常需要设定expire
为了避免错误解锁,需要给一个唯一的value,比对成功才能执行del解锁。lua脚本可以保证原子性
可重入锁的实现也可以通过lua脚本完成
应用2:缓兵之计--延时队列
redis的list数据结构常用来作为异步消息队列使用
使用blpop/brpop可以阻塞获取,可以解决队列为空的问题
当使用阻塞操作时,一旦时间过久,客户端连接就会变成闲置连接,服务器一般会主动断开,阻塞操作会抛出异常,需要特殊处理重试
延时队列可以使用redis的zset实现,消息的到期处理时间作为score。多个线程获取zset到期的任务进行处理
延时队列的具体做法可以在lua脚本中使用zrangebyscore和zrem指令进行任务的获取,利用lua脚本的原子性,可以避免多个线程的并发重复执行命令的问题
如果不使用lua脚本,需要判断zrem是否删除成功来决定是否执行任务
应用3:节衣缩食--位图
redis的位图不是特殊的数据结构,就是普通字符串。redis字符串支持使用getbit/setbit等将字符串(byte数组)看成位数组处理
redis提供位图统计指令bitcount和位图查询指令bitpos,范围参数是字节索引(不是位索引)
应用4:四两拨千斤--HyperLogLog
HLL可以用来解决不需要太精准的统计问题,能够极大节省空间
HLL需要占据一定12k的存储空间,不适用与统计单个用户的相关数据
HLL在计数较小时使用稀疏矩阵存储
应用5:层峦叠嶂--布隆过滤器
布隆过滤器说某个值存在,则可能存在;当它说不存在就肯定不存在
布隆过滤器原理:向布隆过滤器添加key时,使用多个hash函数对key进行hash算得一个整数索引值然后对位数组长度进行驱魔运算得到一个位置,每个hash函数都会得到一个不同的位置,再把位数组的这些位置都置1。判断key是否存在时,需要对key做hash再查看对应位置上的key是否都为1,如果存在一个不为1则key不存在,反之。
应用6:断尾求生--简单限流
滑动窗口法:使用zset做滑动窗口,score为时间戳。用户请求时,根据用户ID和操作,获取对应zset的key,将唯一性数据(可以是时间戳)作为value,当前时间作为score添加到zset;删除小于窗口时间的元素,判断当前zset的元素是否大于限制
因为滑动窗口法需要记录时间窗口内所有的行为,如果这个量很大,会消耗大量的存储空间,不太适合用此方法
local zsetKey = KEYS[1 ]
local curTime = ARGV[1 ]
local value = ARGV[2 ]
local period = ARGV[3 ]
local limit = ARGV[4 ]
redis.call('ZADD' , zsetKey, curTime, value)
redis.call('ZREMRANGEBYSCORE' , zsetKey, 0 , curTime - period)
local count = redis.call('ZCARD' , zsetKey)
if count < limit then
return {['ok' ] = 'OK' }
else
return {['err' ] = 'Over rate limit' }
end
应用7:一毛不拔--漏斗限流
漏斗限流法:初始化漏洞容量,流水速率,剩余空间(初始化为漏洞容量),上一次漏水时间。每次往漏洞中添加时需要先计算上一次添加到现在需要流出的量,然后再添加,判断是否大于容量,大于容量则超过限流
local limitInfo = redis.call('hmget' , KEYS[1 ], 'capacity' , 'passRate' , 'addWater' ,'water' , 'lastTs' )
local capacity = limitInfo[1 ]
local passRate = limitInfo[2 ]
local addWater= limitInfo[3 ]
local water = limitInfo[4 ]
local lastTs = limitInfo[5 ]
if capacity == false then
capacity = tonumber (ARGV[1 ])
passRate = tonumber (ARGV[2 ])
addWater=tonumber (ARGV[3 ])
water = 0
lastTs = tonumber (ARGV[4 ])
redis.call('hmset' , KEYS[1 ], 'capacity' , capacity, 'passRate' , passRate,'addWater' ,addWater,'water' , water, 'lastTs' , lastTs)
return {['ok' ] = 'OK' }
else
local nowTs = tonumber (ARGV[4 ])
local waterPass = tonumber ((nowTs - lastTs)* passRate)
water=math .max (0 ,water-waterPass)
lastTs = nowTs
addWater=tonumber (addWater)
if capacity-water >= addWater then
water=water+addWater
redis.call('hmset' , KEYS[1 ], 'water' , water, 'lastTs' , lastTs)
return {['ok' ] = 'OK' }
end
return {['err' ] = ['Over rate limit' ]}
end
应用8:近水楼台--GeoHash
业界比较通用的地理位置距离排序算法是GeoHash
GeoHash算法:将某个位置的经纬度在网格化地图中得到0和1的串,按照偶数位放经度,奇数位放维度的规则组合经度和维度的二进制串,可将此二进制串转换成数字或字符串。
redis中经纬度使用52位的整数进行编码,放进zset中,value是元素的key,score是GeoHash的52位整数值
redis提供的Geo在内存存储仅仅是一个zset
redis的geo不提供删除功能,可以直接使用zset的删除指令zrem
GEORADIUSBYMEMBER可以实现查询附近的人的功能
在地图应用中,地图数据可能会有百万千万条,如果全部放在一个zset集合中,在redis的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个key的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个key对应的数据量不宜超过1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。所以建议Geo的数据使用单独Redis实例部署,不用集群环境。
应用9:大海捞针--Scan
scan相比keys具备的特点:
通过游标分步进行,不会阻塞线程
提供limit参数,可以控制每次返回结果的最大条数(limit只是个hint,返回的结果可多可少)
同keys一样,提供模式匹配
服务器不需要为游标保存状态
返回的结果可能会有重复,需要客户端去重复
遍历过程中如果有数据修改,修改后的数据能不能遍历到是不确定的
单词返回的结果是空并不意味着遍历结束,而是要看返回的游标是否为0
scan参数limit不是限定返回结果的数量,而是限定服务器单词遍历的字典槽位数量
考虑到扩容与缩容时遍历的准确性,scan遍历顺序采用高位进位加法
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步