《Redis深度历险》应用篇

基础:万丈高楼平地起--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:一毛不拔--漏斗限流

  • 漏斗限流法:初始化漏洞容量,流水速率,剩余空间(初始化为漏洞容量),上一次漏水时间。每次往漏洞中添加时需要先计算上一次添加到现在需要流出的量,然后再添加,判断是否大于容量,大于容量则超过限流
--参数说明,key[1]为对应服务接口的信息,argv1为capacity,argv2为漏水速率,argv3为一次所需流出的水量,argv4为时间戳
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遍历顺序采用高位进位加法
posted @ 2020-12-28 15:46  無花無酒鋤作田  阅读(134)  评论(0编辑  收藏  举报