Redis_进阶
基础数据结构
string
底层是 key-value 的数据结构,一般存储对于的缓存数据比较多。
list
底层是双向链表,一般作为消息队列的使用。
hash
底层是一个 string 类型的 field 和 value 的映射表,一般在缓存的时候用来存放对象。
set
底层是一个不含重复元素且无序的集合,一般当缓存需要存放不能重复的元素且对数据顺序没要求的时候使用。
zset
底层是一个不含重复元素且有序的元素集合。一般当缓存需要存放不能重复的元素且对数据顺序有要求的时候使用。
bitmap
常用于存储连续的二进制数,且只有 0 和 1 两种状态。
除此之外,还有基数统计和地理位置的数据结构
内存淘汰策略
- 从已设置过期时间的数据集中选择最近最少使用的数据淘汰。
- 从已设置过期时间的数据集中选择将要过期的数据淘汰。
- 从已设置过期时间的数据集中选择任意数据淘汰。
- 移除最近最少使用的 Key 。
- 从数据集中任意选择数据集淘汰。
常见的缓存问题
缓存穿透
用户查询数据库不存在的数据,数据也不会在缓存中存储。当用户发起请求,它永远不会访问缓存,数据库压力就会增大。
- 解决方案:
- 参数检验,上层拦截。
- 查询结果为空也做缓存,但有效期设置较短,避免影响正常数据的使用。
缓存击穿
热点数据存储到期,多个线程同时请求热点数据,缓存刚好过期,所有并发都会访问数据库。
- 解决方案:
- 设置热点键。
缓存雪崩
数据未加载到缓存或者缓存大范围失效,导致请求查询数据库。
- 解决方案:
- 事前:高可用缓存。
- 事中:缓存降级。
- 事后:Redis 备份和快速预热。
持久化策略
快照方式
快照方式分为两种,一种是同步处理,但会造成客户端命令的阻塞;一种是异步的方式,但异步方式下当 fork 数据的时候,会占用大量的内存,并且在此期间,如果发生应用数据的修改,那么修改的这份数据会丢失。
追加文件方式
通过日志的方式进行同步,相比较快照的方式,数据较慢。
保证数据的一致性
常使用的方案
更新
先更新数据库在删缓存。
读取
先从缓存中读取数据,如果有就返回,如果没有就查对应的数据库,然后再把数据写入到缓存。
这样的处理会存在一个问题:数据在写入数据库的时候是成功的,但更新缓存失败了,就会出现数据不一致的情况。
处理方式:
- 在更新数据的时候同样也更新缓存,通过加锁的方式进行一个异步的操作。
- 异步重试:把重试请求写到消息队列中,然后通过有专门的消费者重试,直到成功。
- 订阅数据库并更日志,再更新缓存,可以使用 Flume 和 Canal 来实现。
其它方案
- 先删除缓存,在更新数据库,但一般不采用这样的方式,因为如果缓存删除成功了还没更新数据库,那么很容易出现数据丢失的情况,并且 Redis 也没有提供将缓存数据写入数据库的功能。
部署方式
主从复制
将一台 Redis 服务器上的数据复制到其他的 Redis 服务器上,前者为主节点,后者为从节点,数据的复制是单向的,只能从主到从。
这样的部署有一个缺点,那就是如果主服务挂了,需要手动去切换对于的服务器,在此期间会存在数据丢失的情况。
哨兵模式
哨兵模式就是完善主从复制的手动切换,它有一个监控功能会不断监控主节点和从节点是否还在正常运行,当主节点不能工作的时候,会实现自动的故障转移操作,将从节点切换成主节点。
集群模式
主节点的分片和扩展,比起上面两种方式,集群模式的维护成本更高,底层使用哈希槽的方式来实现数据的分发。
实现分布式锁
为什么要使用分布式锁?
为了保证在分布式环境下,对外共享的资源在同一时刻下只有一个客户端可以操作。
加锁
使用 setnx 命令来完成,且需要设置一个唯一标识 value,并设置过期时间。
先说唯一标识 value
如何不设置唯一标识,那么线程在释放锁的时候,就会误释放掉其他线程的锁。
过期时间
如果不设置,那么如果程序挂了,那么这个锁永远都不会被释放。
设置过期时间,怎么保证锁不会提前过期?
可以使用 Redisson 去解决,Redisson 有一个看门狗机制,它是一个后台进程,每隔 10s 就会做一次检查当前线程是否还持有锁,如果有它会做一个自动的延长,延长时间为 30s。
解锁
- 需要拿加锁成功的唯一标识进行解锁,从而保证加锁和解锁的是同一个客户端。
- 解锁操作需要比较唯一标识是否相等,以免释放掉其他线程的锁,相等在执行删除操作。
(这两个操作可以使用 Lua 脚本方式来实现)
BigKey
如果一个 key 对应的 value 占用的内存比较大,那么这个 key 就是一个 BigKey。
- 处理方式:
- 对大 Key 进行拆分
- 对大 Key 进行清理
延伸
Redis 是不是单线程的?
Redis 在 6.0 之前,是单线程的,在 6.0 之后,引入了多线程的概念。
Redis 引入多线程主要是为了提高网络 IO 的读写性能,尽管 Redis 引入了多线程,但执行命令依然是单线程顺序,且 Redis 6.0 多线程是禁用的,只使用主线程,使用的时候需要通过配置项开启,并且要设置线程数,否则不生效。
Redis 为什么使用单线程?
- 单线程可以避免多线程的死锁,上下文切换在资源上的开销。
- 但线程在代码上实现更简单,方便维护。
- Redis 的性能不再 CPU,而在内存和网络。
Redis 为什么那么快?
- Redis 是基于内存的数据存储,这样就避免了磁盘的 IO 开销
- Redis 是单线程实现,避免了多个线程之间对资源的竞争和上下文的切换
- Redis 通过 IO 多路复用技术,让 Redis 实现监听客户端的连接,来实现高性能的网络通信
- Redis 底层的数据结构都是做了进一步优化处理的,使得在数据存储上更加高效。