Redis深水区经验梳理

背景

最近工作中遇到很多redis的问题,索性梳理一下redis的核心的基础技术点,一做分享,二是工作中需要理解透彻,才能解决奇怪的问题。

Redis数据结构

基础数据结构:字符串String、字典Hash、列表List、集合Set、有序集合SortedSet (zset)。
如果只知道基础数据结构,可以说redis只了解的皮毛。

高级数据结构:
1. 位图 :
本质可以理解为字符串,标识字节的每一位是否为 0 或者 1,比如用来统计用户每天是否签到, 就可以考虑用位图的结构。
2. hyperLogLog:
redis用此数据结构 提供不精确的去重计数方案,标准误差是 0.8%左右,可以用于比如统计一个页面的UV等允许误差的统计。 相比较于Set结构,可以省很多的空间,进而提高redis整体的效率。
3. GeoHash:
提供一种地图坐标的应用,比如计算A 与 B 之间的距离,A、B、C、D… 等均输入坐标进去,可以查A附近的人。。。
对没错,朋友圈、附近的人的功能通过该结构就可以实现。

         geoadd  增加
127.0.0.1:6379> geoadd person 118.48205 40.006794 A
(integer) 1
      geodist 距离
     127.0.0.1:6379> geodist person A B km
     geopos 获取位置
     georadiusbymember 查询指定元素附近的其他元素
     ......
  1. PubSub 发布订阅模型
    即Redis 消息队列,一般会使用专用的消息队列,比如kafka、rocketmq,这里不做过多介绍,有兴趣大家可以查一下。

Redis分布式锁的几个小坑

大家经常用的redis分布式锁,一般是通过setNx 命令(带超时时间,防止如果释放失败,可以自动过期),同一时刻只有一个线程可以设置成功,成功返回true,失败返回false。
坑1:主从切换
大家知道Redis高可用一般是Master-Slave结构的,如果Master挂掉了,那么slave会直接提为master,这里就会有一个问题,如果线程1在setNx Master写入成功之后,Master突然挂掉,这个时候Slave成为新的Master,但Master数据此时还没有同步给Slave,就会出现重复加锁的情况,业务使用redis做分布式锁,需要考虑这种情况下的容错处理也可以人工介入,修复影响。
不过也可以 引入 RedLock 算法的libary,它的大致原理是每次加锁会向过半的节点发送请求,全部成功才认为成功,删除的时候,同样要删除所有加锁节点,效率会降低很多。

坑2:超时问题
如果请求在加锁和释放锁之间的业务逻辑重,执行过长,以至于超出了锁的expire time,就会出现问题。因为这时候锁过期了,会被清除掉,此刻第二个线程B重新持有了这把锁,但是紧接着第一个线程A执行完了相关的业务逻辑,就把锁给释放了(其实此时是线程B加的锁),第三个线程C就会在第二个线程逻辑执行过程中又拿到了锁…
为了避免这个问题有两个方案:

  1. Redis 分布式锁不要用于业务比较重的任务。如果真的偶尔出现了,数据出现的错乱需要人工介入解决,修复错乱数据。
  2. 比较麻烦,需要加锁的同时,记录是谁加的这把锁,即在setNx的时候,放入一个值,比如userid,在删除锁的时候,匹配这个value跟自己是否相等,相等的时候才进行锁的释放。

Redis过期策略

Redis本身将每个设置了过期时间的 key 放入到一个独立的字典中,鉴于单线程的特性,它采取两种方式进行过期key的收割,一个是定时删除、一个是惰性删除。
1. 定时删除:
顾名思义。定时遍历所有过期key的字典,进行删除。但是他不是每次删除全部,主要是如果过期key很多,根据reids单线程特性,大量的过期扫描岂不是要延迟很久。redis采取了一种贪心策略,会从先取出一部分key,比如20个,看这些key是否过期,清除掉过期的key,如果过期key的比率较高,比如超过 25%,会重复再次获取20个key,再处理。为了保证redis的高性能,即使过期key的比率较高,也不会无限循环,这里每次扫描处理也控制在一定时间内(25ms)具体大家可以查阅更详细的文档。

2.惰性删除
惰性删除指的是在请求访问这个 key 的时候, redis再 对 key 的过期时间进行检查,如果过期
了就即刻删除。可以认为定时删除是集中一起处理,惰性删除是零散的处理。

了解了过期策略,这里大家也要注意,对于key的过期,大家尽量避免大量的key在同一时刻过期,比如签到,一般大家都喜欢凌晨24点过期,这个时候,其实可以给每个人的 过期增加一个随机数,让过期错开。

Redis高性能的秘密

  1. Redis是单线程的
    提到这里这里大家可能觉得奇怪,单线程怎么会是原因呢,其实单线程高性能的不只redis,比如node.js nginx 都是单线程高性能的代表。
    单线程没有CPU切换,没有锁争用的问题(假设不是单线程的redis的hash set 这些结构,仅仅处理并发就要耗费非常多的耗时),但是redis单线程快,不仅仅是这些,还有关键的因素,
    一是所有的数据都在内存操作,速度极快 二是IO采用非阻塞IO,谈起非阻塞IO 我们可能会想起操作系统的select(基本已经弃用),epoll模型 、kqueue等,其实redis底层依赖的确实如此,只是redis的事件处理更加高效,比如将所有客户端的指令放入队列,先到先服务,详细的大家可以深入了解。
  2. Redis存储小对象压缩
    这里不得不提的就是 ziplist的结构,它是一种紧凑的字节数组结构,redis的很多数据结构比如zset、hash如果比较小都会通过ziplist压缩实现, 节省内存,读取速率也很高。

由上述两点,其实我们如果要保持redis的高性能的使用,应该避免大Value的情况,大Value会增加响应延迟,那么大Value的情况如何处理呢:
这里主要说两点:
1. 拆分key,根据业务视角将单Key分拆成多个Key, 平摊到多个redis实例中
2. 对复杂数据结构的缓存,拆成一个个子项处理

Redis的持久化

提到目前市面很多的缓存比如memcache等,不得不提的就是redis的持久化机制。
redis有两种持久化机制:
1.SNAPSHOT 快照
bgsave全量备份,这里你可能会有疑惑,redis在响应请求,怎么处理快照备份,这里其实redis 会fork一个子进程处理这个事情,同时呢利用 COW(copy on write)的机制进行数据备份,简单讲,如果数据没有改动备份进程 和 redis主进程使用的同一份数据,一旦有写入,那么redis主进程会拷贝一份出来,只在那一块内存写入(其实这里涉及了操作系统页存储,取出一页复制一份,不影响备份进程),子进程备份的数据不变还是那一时刻的,慢慢主进程 和 子进程数据分开。
2.AOF日志
增量备份,aof日志其实是一个个指令文本,redis根据指令执行备份。这里有一个问题是,aof其实是写入了内存缓冲区,定期通过 fsync同步回磁盘,这定期的时间就是,如果出现宕机,可能丢失的时间段的数据,比如1分钟同步一次,那么down机时就有可能有一份钟的数据丢失。

鉴于上述特性,一般我们会在redis实例重启时,使用bgsave持久化文件重新构建redis内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。

总结

篇幅问题,笔者不可能所有的问题都能面面俱到,这里权当抛转引玉,其实redis的基础数据结构的实现也是比较有意思的,大家有兴趣可以深入研究下。

猜你喜欢

转载自blog.csdn.net/baoxue2008/article/details/106983475