Redis核心技术与实战

Redis功能

hyperloglog

用户统计基数的数据集合类型

pfadd 增加计算 pfcount 统计数据

example: pfadd uv user1 pfcout uv

pfmerge 合并多个值

code:

PFADD page1:uv user1 user2 user3

PFCOUNT page1:uv

布隆过滤器

场景:判断某个值在不在集合中,有误差,比如推荐系统,过滤用户已经看过的新闻

redis命令

添加元素 bf.add news user1

判断是否在集合中 bf.exists news users

添加多个元素 bf.madd news user1 user2 user3

判断多个元素是否在集合中 bf.mexists news user1 user2 user3

redis做消息队列

基于List做消息队列
LPUSH 入队,RPOP出队
BRPOP阻塞式读取,可以更省CPU开销
如果读取消息后,redis挂了,可以使用BRPOPLPUSH命名,让消费者程序从一个List中读取消息,同时Redis会把这个图消息再插入到另一个List留存,这样子,redis重启后,就可以从备份的List中读取消息并进行处理

基于Streams的消息队列解决方案
Redis5.0才有
操作命令:
XADD:  插入消息,保证有序,可以自动生成全局唯一ID
XREAD: 用于读取消息,可以按ID读取消息
XREADGROUP: 按消费组形式读取消息
XPENDING和XACK: XPENDING命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,XACK命令用于向消息队列确认消息处理已完成

// * 号让系统自动生成id号,也可以自己指定
XADD mqstream * repo 5
"1599203861727-0"



// 从id号开始读取后续的所有数据,block类似BRPOP,阻塞读取,block后面设置的是时间,超过这个时间
// 还没有读取到,则返回空
XREAD BLOCK 100 STREAMS  mqstream 1599203861727-0
1) 1) "mqstream"
   2) 1) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"
      2) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"
      3) 1) "1599274927910-0"
         2) 1) "repo"
            2) "1"
            
            
            
// $ 表示读取最新的消息
XREAD block 10000 streams mqstream $
(nil)
(10.00s)



Streams的消费组的概念

todo: 消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被消费组内的其他消费者读取了
// 创建一个group1的消费组, 该消费组的消息队列为mqstream
XGROUP create mqstream group1 0
OK


// 让group1消费组里的消费者consumer1从mqstream中读取所有消息
// ‘>’ 表示从第一条尚未被消费的消息开始读取
127.0.0.1:6379> xreadgroup group group1 consumer1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1630813716027-0"
         2) 1) "repo"
            2) "5"
      2) 1) "1630813794727-0"
         2) 1) "repo"
            2) "2"
      3) 1) "1630813797519-0"
         2) 1) "repo"
            2) "1"
      4) 1) "1630814298480-0"
         2) 1) "repo1"
            2) "1"
            
            
            
// group2中的consumer1,2,3各自读取一条消息
XREADGROUP group group2 consumer1 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599203861727-0"
         2) 1) "repo"
            2) "5"

XREADGROUP group group2 consumer2 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599274912765-0"
         2) 1) "repo"
            2) "3"

XREADGROUP group group2 consumer3 count 1 streams mqstream >
1) 1) "mqstream"
   2) 1) 1) "1599274925823-0"
         2) 1) "repo"
            2) "2"
            
            
//一旦某一条消息被处理了,消费者就可以使用xack通知Streams把这条消息删除

 XACK mqstream group2 1599274912765-0
(integer) 1
XPENDING mqstream group2 - + 10 consumer2
(empty list or set)

清理内存碎片

查看内存碎片

INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86

这里有一个 mem_fragmentation_ratio 的指标,它表示的就是 Redis 当前的内存碎片率。那么,这个碎片率是怎么计算的呢?其实,就是上面的命令中的两个指标 used_memory_rss 和 used_memory 相除的结果。


mem_fragmentation_ratio = used_memory_rss/ used_memory

首先,Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes,命令如下:config set activedefrag yes

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。
  • active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;
  • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。

缓存区

输入缓冲区溢出:

原因:

  1. 写入了 bigkey,比如一下子写入了多个百万级别的集合类型数据;
  2. 服务器端处理请求的速度过慢,例如,Redis 主线程出现了间歇性阻塞,无法及时处理正常发送的请求,导致客户端发送的请求在缓冲区越积越多。 解决办法
  3. 调整客户端缓存大小,这个一般是1GB,但是对于一般的生成环境够用了,也没有参数来调整
  4. 避免客户端写入bigkey

输出缓冲区溢出:

原因:

  1. 服务器端返回 bigkey 的大量结果;
  2. 执行了 MONITOR 命令;
  3. 缓冲区大小设置得不合理。

解决办法:

  1. 避免 bigkey 操作返回大量数据结果;
  2. 避免在线上环境中持续使用 MONITOR 命令。
  3. 使用 client-output-buffer-limit 设置合理的缓冲区大小上限,或是缓冲区连续写入时间和写入量上限。

主从复制也会导致缓存区溢出

  1. 控制主节点保存的数据量大小
  2. 设置合理的复制缓存区大小
  3. 控制从节点数量

Redis缓存

Redis用户缓存需要解决如下几个问题:

  1. Redis 缓存具体是怎么工作的?
  2. Redis 缓存如果满了,该怎么办?
  3. 为什么会有缓存一致性、缓存穿透、缓存雪崩、缓存击穿等异常,该如何应对?
  4. Redis 的内存毕竟有限,如果用快速的固态硬盘来保存数据,可以增加缓存的数据量,那么,Redis 缓存可以使用快速固态硬盘吗?

解决缓存问题

解决缓存不一致

操作顺序 是否有并发请求 潜在问题 现象 应对方案
先删除缓存值,再更新数据库 无 缓存删除成功,但数据库更新失败 应用从数据库读取到旧数据 重试数据库更新
先删除缓存值,再更新数据库 有 缓存删除后,尚未更新数据库,有并发读请求 并发请求从数据库读到旧值,并且更新到缓存,导致后续请求都读取旧值 延迟双删除
先更新数据库,再删除缓存 无 数据库更新成功,但缓存删除失败 应用从缓存读到旧数据 重试缓存删除
先更新数据库,再删除缓存 有 数据库更新成功后,尚未删除缓存,有并发读请求 并发请求从缓存中读到旧值 等待缓存删除完成,期间会有不一致数据短暂存在

解决缓存雪崩, 缓存击穿,缓存穿透

| 问题 | 原因 | 应对方案 |
|:--|:--|:--|:--| | 缓存雪崩 | * 大量数据同时过期
* 缓存实例宕机 | 1. 给缓存数据的过期时间加上小的随机数
2. 服务降级
3. 服务熔断
4. 请求限流
5. Redis缓存主从集群 |
|缓存击穿 | 访问非常频繁的热点数据过期 |不给热点数据设置过期时间,一直保留 |
|缓存穿透|缓存和数据库中都没有要访问的数据|1. 缓存空值或缺省值
2. 请使用布隆过滤器快速判断 3. 请求入口前端对请求合法性进行检查|

解决缓存污染问题

那什么是缓存污染呢?在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。

如何解决:
LRU 策略和 LFU 策略

大内存Redis

使用ssd解决大内存Redis问题 https://github.com/OpenAtomFoundation/pika

Redis应对并发

  1. 使用锁
  2. 使用原子操作, Lua脚本

分布式锁

为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

ACID

Redis通过MULTI, EXEC, DISCARD,WATCH四个命令来支持事务机制

命令 作用
MULTI 开启一个事务
EXEC 提交事务,从命令队列中取出提交的操作命令,进行实际执行
DISCARD 放弃一个事务,清空命令队列
WATCH 检测一个或多个键的值在事务执行期间是否发生变化,如果发生变化,那么当前事务放弃执行

Redis 的事务机制可以保证一致性和隔离性,但是无法保证持久性。不过,因为 Redis 本身是内存数据库,持久性并不是一个必须的属性,我们更加关注的还是原子性、一致性和隔离性这三个属性。原子性的情况比较复杂,只有当事务中使用的命令语法有误时,原子性得不到保证,在其它情况下,事务都可以原子性执行。

主从故障

  • 主从数据不一致。Redis 采用的是异步复制,所以无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。我给你提供了应对方法:保证良好网络环境,以及使用程序监控从库复制进度,一旦从库复制进度超过阈值,不让客户端连接从库。
  • 对于读到过期数据,这是可以提前规避的,一个方法是,使用 Redis 3.2 及以上版本;另外,你也可以使用 EXPIREAT/PEXPIREAT 命令设置过期时间,避免从库上的数据过期时间滞后。不过,这里有个地方需要注意下,因为 EXPIREAT/PEXPIREAT 设置的是时间点,所以,主从节点上的时钟要保持一致,具体的做法是,让主从节点和相同的 NTP 服务器(时间服务器)进行时钟同步。

脑裂

所谓的脑裂,就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主库因为某些原因卡住了 15s,导致哨兵判断主库客观下线,开始进行主从切换。同时,因为原主库卡住了 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。