Redis的使用场景有哪些?

缓存分布式锁、计数器、消息队列、延迟队列等等。比较重要的就两个方面:

  • 缓存:穿透、击穿、雪崩、双写一致、持久化、数据过期、数据淘汰
  • 分布式锁:setnx、redisson

缓存穿透问题

即查询一个数据库中不存在的数据,而引发的问题。
请求先去查询 Redis 缓存,发现缓存中没有,然后去查找数据库,结果数据库中也没有,所以不会写入缓存,这就导致每次请求都会去查询数据库。

和缓存击穿的区别在于,击穿主要是因为 Key 过期了。

解决方案一:缓存空数据。

数据库中查询为空,则把这个空结果进行缓存。优点是简单,缺点是消耗内存,并且可能会发生不一致的问题。

解决方案二:布隆过滤器。

查询 Redis 之前,先经过布隆过滤器,若不存在则直接返回,不会去查询 Redis,也不会去查数据库。优点是内存占用较少,缺点是实现复杂并存在误判。

布隆过滤器

布隆过滤器依赖着 bitmap(位图)这样的数据结构,它用于快速检索一个元素是否存在在一个集合中。
原理是将数据用多个哈希函数进行计算,并将位图数组的对应位置修改为 1。当过滤时,同样用多个哈希函数进行计算,若发现计算出的对应 bitmap 数组位置出现了 0,则该数据一定不存在,反之有可能存在

布隆过滤器是存在误判问题的,一般 5% 左右的误判率是可以接受的。其实,bitmap 数组越大,误判率就越小,但是同时也带来了更多的内存消耗。

缓存击穿

给一个 Key 设置了过期时间,恰好在 Key 过期的时候有大量的并发请求,这些请求会瞬间涌向数据库当中,将数据库压垮。

数据库其实是有数据的,这点和缓存穿透不同。

解决方案一:互斥锁。

线程 1 查询缓存,未命中,则会获得互斥锁,然后查询数据库重建缓存数据,即将查询到的数据写入缓存,最后释放锁。其他线程到达后需要等待锁的释放。优点是强一致性,但性能比较差。

解决方案二:逻辑过期。(不设置过期时间)

在数据字段中增加过期时间字段,线程 1 查询时若发现逻辑时间过期,则获得互斥锁,然后新开一个线程 2,去重建缓存数据并重置逻辑过期时间,而线程 1 会返回过期数据。其他线程若获取互斥锁失败,则会返回过期数据。优点是高可用且性能比较好,但缺点是一致性比较差。

两种解决方案的区别在于,互斥锁方案中,当一个线程获取了互斥锁,其他线程必须等待该线程重建缓存数据。而逻辑过期方案中,当一个线程获取了互斥锁,则其他线程会拿到过期数据,除非缓存已重建完毕。

缓存雪崩

在同一时段内大量的缓存 Key 失效或者 Redis 服务宕机,导致大量请求到达数据库。

缓存击穿的区别在于缓存击穿是一个 Key 过期,而缓存雪崩是大量 Key 过期。

解决方案一:给不同的 Key 设置不同的过期时间。
解决方案二:Redis 集群。(哨兵模式,集群模式)
解决方案三:给缓存业务添加降级限流策略。

同样适用于缓存穿透和缓存击穿。

解决方案四:多级缓存。(guava,caffeine)

MySQL的数据如何与Redis的数据同步?即双写一致性问题

双写一致性:当修改了数据库中的数据,也要同时更新缓存的数据。

一般的 Redis 的读操作:缓存命中,则直接返回。缓存未命中则查询数据库,写入缓存,设定超时时间。

双写一致的延迟双删策略:在写数据库的时候,先删除缓存,再修改数据库,之后延时删除缓存。
为什么要删除两次缓存?为什么要延时删除?

  • 防止脏数据。延时是为了让主从数据库均完成了数据修改。

双写一致的分布式锁策略:即采用 Redisson 提供的两个锁。

  • 共享锁:读锁,其他线程可以共享读,但不能写
  • 排他锁:写锁,阻塞其他线程读写操作

读写锁策略能够保证强一致性,但性能比较低。

双写一致的异步通知策略:数据修改后,向消息队列发送消息,由缓存订阅该消息。

双写一致的基于Canal的异步通知策略:伪装为 MySQL 的一个从节点,canal 通过读取 binlog 日志更新缓存。

持久化

在 Redis 中提供了两种数据持久化的方式:RDB,以及 AOF
RDB 也叫 Redis 数据快照,简单来说就是把内存中的所有数据都记录到磁盘中,当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。

save 命令会阻塞所有命令。besave 命令会开启子进程执行 RDB,避免主进程受到影响。

RDB 的执行原理:bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据,完成 fork 后读取内存数据并写入 RDB。fork 采用的是 copy-on-write 技术:

  • 当主进程执行操作时,访问共享内存
  • 当主进程执行操作时,则会拷贝一份数据,执行写操作

AOF即追加文件(Append Only File).Redis 处理的每一个写命令都会记录在 AOF 文件,可以看作是命令日志文件。

AOF 默认是关闭的,需要将 appendonly 设置为 yes 进行开启。

因为是记录命令,AOF 文件会比 RDB 文件大得多,而且 AOF 文件会记录对同一个 Key 的多次写操作,通过执行 bgwriteaof 命令,可以让 AOF 文件执行重写功能,用最少的命令达到同样的效果。

Redis的数据过期问题

Redis 会对数据设置有效时间,数据过期以后,就需要将数据从内存中删除掉,这些删除规则就被称为数据的删除策略(数据过期策略)。

  • 惰性删除:当需要 Key 时,检查是否过期,如果过期,则删除,反之返回

    如果一个 Key 早已过期,但一直未使用,那么该 Key 就会一致存在,浪费内存。

  • 定期删除:每隔一段时间,就对一些 Key 进行检查,删除过期 Key

    分为 SLOW 模式和 FAST 模式,SLOW 模式是定时任务,执行频率默认为 10hz,每次不超过 25ms。FAST 模式执行频率不固定,但两次间隔不低于 2ms。

Redis 的过期删除策略是:惰性删除 + 定期删除配合使用。

Redis的数据淘汰

引入:加入缓存过多,而内存有限,内存被占满了怎么办?
数据淘汰指的是当 Redis 的内存不够用了,此时 Redis 就需要按照某种规则将内存中的数据删除掉。Redis 支持 8 种不同策略来选择要删除的 Key:

  • noeviction:不淘汰任何 Key,内存满时不允许写入新数据,默认采用这种策略
  • volatile-ttl:对设置了 TTL 的 Key,比较 Key 的剩余 TTL 值,TTL 值越小越先淘汰
  • always-random:对全体 Key,随机进行淘汰
  • volatile-random:对设置了 TTL 的 Key,随机进行淘汰
  • allkeys-lru:对全体 Key,基于 LRU 算法进行淘汰(优先建议使用,可以保留热点数据
  • volatile-lru:对设置了 TTL 的 Key,基于 LRU 算法进行淘汰
  • allkeys-lfu:对全体 Key,基于 LFU 算法进行淘汰
  • volatile-lfu:对设置了 TTL 的 Key,基于 LFU 算法进行淘汰

LRU 是最近最少使用,LFU 是最近最不常用。

Redis分布式锁的应用场景

抢优惠券时的场景,考虑目前优惠券只剩下 1 张,线程 1 和线程 2 同时查询,发现仍有 1 张优惠券,然后同时抢走了这一张优惠券。
怎么解决这样类似的超卖问题?

  • 加互斥锁,虽然能解决问题,但由于集群的存在,往往会出现问题
  • 所以需要分布式锁,而不是一个本地的互斥锁

Redis分布式锁的setnx实现

setnx 命令即 SET if not exists。

  • 获取锁:SET lock value NX EX 10(NX 是互斥,EX 是设置超时时间)
  • 释放锁:DEL key

如何合理地控制锁的有效时长?

  • 根据业务执行时间预估
  • 给锁续期

Redisson实现的分布式锁

线程想要获取锁,获取锁后就可以操作 Redis。核心在于,线程加锁后,Watch Dog 会每隔 releaseTime / 3 的时间做一次续期。而释放锁是由线程手动释放的。
其他线程来时,若发现有其他线程持有锁,则会不断 while 循环尝试去获取锁。为了防止无限等待,可以通过设置 if 函数,来在固定的循环次数后退出。

该实现中加锁、设置过期时间等操作都是基于 Lua 脚本实现的。