• 《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遍历顺序采用高位进位加法
  • 相关阅读:
    获取有关控件的坐标
    Android PopupWindow的使用和分析
    Android RecyclerView 使用完全解析 体验艺术般的控件
    TextView中显示价格并且中间直接有一个横线
    Android Studio
    移动开发】Android中三种超实用的滑屏方式汇总(ViewPager、ViewFlipper、ViewFlow)
    Android中visibility属性VISIBLE、INVISIBLE、GONE的区别
    理解Java的IO流 2015年8月6日 21:30:38
    算法导论学习随笔——第七章 快速排序
    oj 1031 random permutation
  • 原文地址:https://www.cnblogs.com/prelude1214/p/14201959.html
Copyright © 2020-2023  润新知