主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据。
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
搭建分片集群
计划部署的节点信息如下:
容器名 | 角色 | IP | 映射端口 |
r1 | master | 192.168.21.129 | 7001 |
r2 | master | 192.168.21.129 | 7002 |
r3 | master | 192.168.21.129 | 7003 |
r4 | slave | 192.168.21.129 | 7004 |
r5 | slave | 192.168.21.129 | 7005 |
r6 | slave | 192.168.21.129 | 7006 |
分片集群中的Redis节点必须开启集群模式,一般在配置文件中添加下面参数:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
cluster-enabled
:是否开启集群模式cluster-config-file
:集群模式的配置文件名称,无需手动创建,由集群自动维护cluster-node-timeout
:集群中节点之间心跳超时时间
一般搭建部署集群肯定是给每个节点都配置上述参数,不过考虑到我们计划用docker-compose
部署,因此可以直接在启动命令中指定参数
在虚拟机的/root
目录下新建一个redis-cluster
目录,然后在其中新建一个docker-compose.yaml
文件,内容如下:
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r4:
image: redis
container_name: r4
network_mode: "host"
entrypoint: ["redis-server", "--port", "7004", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r5:
image: redis
container_name: r5
network_mode: "host"
entrypoint: ["redis-server", "--port", "7005", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r6:
image: redis
container_name: r6
network_mode: "host"
entrypoint: ["redis-server", "--port", "7006", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
注意:使用Docker部署Redis集群,network模式必须采用host
进入/root/redis-cluster
目录,使用命令启动redis:
docker-compose up -d
启动成功,可以通过命令查看启动进程:
ps -ef | grep redis
# 结果:
root 4822 4743 0 14:29 ? 00:00:02 redis-server *:7002 [cluster]
root 4827 4745 0 14:29 ? 00:00:01 redis-server *:7005 [cluster]
root 4897 4778 0 14:29 ? 00:00:01 redis-server *:7004 [cluster]
root 4903 4759 0 14:29 ? 00:00:01 redis-server *:7006 [cluster]
root 4905 4775 0 14:29 ? 00:00:02 redis-server *:7001 [cluster]
root 4912 4732 0 14:29 ? 00:00:01 redis-server *:7003 [cluster]
可以发现每个redis节点都以cluster模式运行。不过节点与节点之间并未建立连接。
接下来,我们使用命令创建集群:
# 进入任意节点容器
docker exec -it r1 bash
# 然后,执行命令
redis-cli --cluster create --cluster-replicas 1 \
192.168.21.129:7001 192.168.21.129:7002 192.168.21.129:7003 \
192.168.21.129:7004 192.168.21.129:7005 192.168.21.129:7006
- redis-cli --cluster:代表集群操作命令
- create:代表是创建集群
- --cluster-replicas 1 :指定集群中每个master的副本个数为1
接着,我们可以通过命令查看集群状态:
redis-cli -p 7001 cluster nodes
散列插槽
在Redis集群中,共有16384个hash slots,集群中的每个master节点都会分配一定数量的hash slots:
Redis数据不是与节点绑定,而是与插槽slot绑定。当我们读写数据时,Redis基于CRC16算法对key做hash运算,得到的结果与16384取余,就计算出了这个key的slot值。然后到slot所在的Redis节点执行读写操作。
redis在计算key的hash值是不一定是根据整个key计算,分两种情况∶
- 当key中包含0时,根据{之间的字符串计算hash slot
- 当key中不包含0时,则根据整个key字符串计算hash slot
例如: key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。
Redis数据结构
RedisObject
Redis中的任意数据类型的键和值都会被封装为一个RedisObject,也叫做Redis对象,源码如下(Redis用c语言写的):
Redis的编码方式
Redis中会根据存储的数据类型不同,选择不同的编码方式,共包含12种不同类型:
五种数据结构
Redis中会根据存储的数据类型不同,选择不同的编码方式。每种数据类型的使用的编码方式如下:
SkipList
SkipList(跳表)首先是链表,但与传统链表相比有几点差异:
元素按照升序排列存储
节点可能包含多个指针,指针跨度不同。
最多32级指针
特点:
- 跳跃表是一个有序的双向链表
- 每个节点都可以包含多层指针,层数是1到32之间的随机数
- 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
- 增删改查效率与红黑树基本一致,实现却更简单。但空间复杂度更高
SortedSet
SortedSet数据结构的特点是:
- 每组数据都包含score和member
- member唯一
- 可根据score排序
SortedSet的底层数据结构是怎样的?
- 首先SortedSet需要能存储score和member值,而且要快捷的根据member查询score,因此底层有一个哈希表以member为键,以score为value
- 其次SortedSet还需要能根据score排序,因此底层还维护了一个跳表。
- 当需要根据member查询score时,就去哈希表中查询;当需要根据score排序查询时,则基于跳表查询
Redis内存回收
过期KEY处理
Redis提供了expire命令,给key设置TTL(存活时间)
可以发现,当key的TTL到期以后,再次访问name返回的是nil,说明这个key已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。
Redis是如何知道一个key是否过期
Redis的本身是键值型数据库,其所有数据都存在一个redisDB的结构体中,其中包含两个哈希表:.
- dict:保存Redis中所有的键值对
- expires:保存Redis中所有的设置了过期时间的KEY及其到期时间(写入时间+TTL)
是不是TTL到期就立即删除了
Redis并不会实时监测key的过期时间,在key过期后立刻删除。而是采用两种延迟删除的策略:
- 惰性删除:当有命令需要操作一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
- 周期删除:通过一个定时任务,周期性的抽样部分有TTL的key,如果过期则执行删除。
周期删除的定时任务执行周期有两种:
- SLOW模式:默认执行频率为每秒10次,但每次执行时长不能超过25ms,受server.hz参数影响。
- FAST模式:频率不固定,跟随Redis内部I0事件循环执行。两次任务之间间隔不低于2ms,执行时长不超过1ms
内存淘汰策略
内存淘汰:就是当Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放更多内存的流程。Redis会在每次处理客户端命令时都会对内存使用情况做判断,如果必要则执行内存淘汰。内存淘汰的策略
noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
allkeys-random:对全体key,随机进行淘汰。也就是直接从db->dict中随机挑选
volatile-random:对设置了TTL的key,随机进行淘汰。也就是从db->expires中随机挑选。
allkeys-Iru:对全体key,基于LRU算法进行淘汰
volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰
allkeys-lfu:对全体key,基于LFU算法进行淘汰
volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰(推荐)
LRU(Least Recently Used),最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFu(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
LFU的访问次数之所以叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过运算:
1)生成[0~1)之间的随机数R
2)计算1/(旧次数*lfu_log_factor + 1),记录为P,lfu_log_factor默认为10
3)如果R<P,则计数器+1,且最大不超过255
4)访问次数会随时间衰减,距离上一次访问时间每隔lfu_decay_time分钟(默认1),计数器-1
缓存问题
缓存一致性
我们先看下目前企业用的最多的缓存模型。缓存的通用模型有三种:
Cache Aside
:有缓存调用者自己维护数据库与缓存的一致性。即:
- 查询时:命中则直接返回,未命中则查询数据库并写入缓存
- 更新时:更新数据库并删除缓存,查询时自然会更新缓存
Read/Write Through
:数据库自己维护一份缓存,底层实现对调用者透明。底层实现:
- 查询时:命中则直接返回,未命中则查询数据库并写入缓存
- 更新时:判断缓存是否存在,不存在直接更新数据库。存在则更新缓存,同步更新数据库
Write Behind Cahing
:读写操作都直接操作缓存,由线程异步的将缓存数据同步到数据库
那到底是先更新数据库再删除缓存,还是先删除缓存再更新数据库呢?
假如先删除Redis
现在假设有两个线程,一个来更新数据,一个来查询数据。
- 线程1删除缓存后,还没来得及更新数据库,
- 此时线程2来查询,发现缓存未命中,于是查询数据库,写入缓存。由于此时数据库尚未更新,查询的是旧数据。也就是说刚才的删除白删了,缓存又变成旧数据了。
所以先操作数据库,再删Redis
当然这种也有并发安全问题,但是概率极低
缓存一致性策略的最佳实践方案:
1.低一致性需求:使用Redis的key过期清理方案
2.高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
- 先写数据库,然后再删除缓存.
- 要确保数据库与缓存操作的原子性
缓存穿透
缓存穿透是指客户端请求的数据在数据库中根本不存在,从而导致请求穿透缓存,直接打到数据库的问题。
常见的解决方案有两种:
缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗布隆过滤
布隆过滤
优点:内存占用较少,没有多余key
缺点:
实现复杂
存在误判可能
布隆过滤是一种数据统计的算法,用于检索一个元素是否存在一个集合中。但是布隆过滤无需存储元素到集合,而是把元素映射到一个很长的二级制数位上。
- 首先需要一个很长很长的二级制数,默认每一位都是0
- 然后需要N个不同算法的哈希函数
- 将集合中的元素根据N个哈希函数做运算,得到N个数字,然后将每个数字对应的bit位标记为1
- 要判断某个元素是否存在,只需要把元素按照上述方式运算,判断对应的bit位是否是1即可
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
由于我们采用的是Cache Aside
模式,当缓存失效时需要下次查询时才会更新缓存。当某个key缓存失效时,如果这个key是热点key,并发访问量比较高。就会在一瞬间涌入大量请求,都发现缓存未命中,于是都会去查询数据库,尝试重建缓存。可能一瞬间就把数据库压垮了。
解决方案:
- 互斥锁
- 逻辑过期