本笔记是学习Redis视频教程整理:https://www.bilibili.com/video/BV1Rv41177Af?spm_id_from=333.337.search-card.all.click
6. Redis的发布和订阅
-
什么是发布和订阅
Redis发布订阅(pub/sub)是一种消息通信模式:发布者(pub)发送消息,订阅者(sub)接收消息
Redis客户端可以订阅任意数量的频道
-
Redis的发布和订阅
-
客户端可以订阅频道如下图
-
当给这个频道发布消息后,消息就会发送给订阅的客户端
-
-
发布和订阅的命令行实现
-
打开一个客户端订阅channel1
使用docker启动两个容器:
docker exec -it redis1 /bin/bash
,敲入命令连接redis:redis-cli
消息订阅命令:
subscribe channel1 channel2 ...
可以订阅多个频道
-
打开另一个客户端,给channel1发送消息:helloRedis
消息发布命令:
publish channel 消息内容
,返回值表示有几个订阅者 -
切回第一个客户端,也就是订阅者窗口,可以看到收到了消息
-
-
常用命令
-
subscribe channel [channel ...]
:订阅给定的一个或多个频道的消息127.0.0.1:6379> subscribe channel1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" # 返回值类型:显示订阅成功 2) "channel1" # 订阅的频道名称 3) (integer) 1 # 目前已订阅的频道数量 1) "message" # 返回值类型:消息 2) "channel1" # 来源,从哪个频道发送过来 3) "helloRedis" # 消息内容
-
publish channel message
:发布消息到指定的频道127.0.0.1:6379> publish channel1 helloRedis (integer) 1 # 接收消息的订阅者数量
-
psubscribe pattern [pattern ...]
:订阅一个或多个符合给定模式的频道每个pattern以
*
作为匹配符,比如it*
匹配所有以it开头的频道,news.*
匹配所有以news.
开头的频道,等等。
-
7. Redis事务操作
-
Redis的事务定义
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令,防止别的命令插队
-
Multi,Exec,discard
从输入
Multi
命令开始,输入的命令都会一次进入命令队列中,但不会执行,直到输入Exec
后,Redis会将之前的命令队列中的命令依次执行。组队过程中可以通过
discard
来放弃组队。redis事务分2个阶段:组队阶段,执行阶段
- 组队阶段:只是将所有命令加入命令队列
- 执行阶段:依次执行队列中的命令,在执行这些命令的过程中,不会被其他客户端发送的请求命令插队或打断
-
使用示例
-
组队成功,提交成功
127.0.0.1:6379> keys * (empty array) 127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k1 v1 # 多了个TX显示 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> exec # 命令队列的两条命令都执行成功 1) OK 2) OK 127.0.0.1:6379> keys * 1) "k1" 2) "k2"
-
组队阶段报错,提交失败
127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set m1 v1 QUEUED 127.0.0.1:6379(TX)> set m2 (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379(TX)> set m3 v3 QUEUED 127.0.0.1:6379(TX)> exec (error) EXECABORT Transaction discarded because of previous errors.
-
组队成功,提交有成功,有失败情况
127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set m1 v1 QUEUED 127.0.0.1:6379(TX)> incr m1 QUEUED 127.0.0.1:6379(TX)> set m2 v2 QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) (error) ERR value is not an integer or out of range 3) OK 127.0.0.1:6379> keys * 1) "m2" 2) "k1" 3) "k2" 4) "m1"
-
-
事务的错误处理
上面示例已经演示了事务错误可能的情况了。
-
情况1:组队中只要有一条命令有误,所有命令取消执行
-
情况2:组队中没有问题,执行中部分成功部分失败
命令在组队过程中没有问题,但是执行时出错,会导致部分成功,部分失败,失败的命令不会影响成功命令的结果执行。
-
-
事务冲突的问题
-
例子
想象有一个场景:你的账户中只有10000,有多个人使用你的账户,同时参加双十抢购
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
三个请求同时处理,看到金额都是10000,都比要操作的金额大,都去执行各自的修改余额操作,最后余额变成了-4000,这肯定是有问题的。
-
-
事务冲突的解决方案
-
悲观锁
悲观锁(Pessimistic Lock),顾名思义就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁。
-
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的
3.watch key [key...]
在执行multi之前,先执行
watch key1 [key2]
可以监视一个或多个key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断,事务的所有操作将被取消执行开启两个redis客户端:
第一个客户端:
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set salary 1000 OK 127.0.0.1:6379> watch salary OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> incrby salary 100 QUEUED 127.0.0.1:6379(TX)> set name zhangsan QUEUED 127.0.0.1:6379(TX)> decrby salary 200 QUEUED 127.0.0.1:6379(TX)> exec # 此时已经被第二个客户端修改了salary的值了,执行失效,返回nil (nil) 127.0.0.1:6379> get salary # 获取salary的值,变成1500,是第二个客户端修改的结果 "1500"
第二个客户端:
127.0.0.1:6379> incrby salary 500 # 在第一个客户端执行exec之前执行操作 (integer) 1500 127.0.0.1:6379> get salary "1500"
-
unwatch
命令:取消监视取消watch命令对所有key的监视。
如果在执行watch命令之后,exec命令或discard命令先被执行了的话,那就不需要再指向unwatch命令了。
因为EXEC命令会执行事务,因此watch命令的效果已经产生了;
而DISCARD命令在取消事务的同时也会取消所有对key的监视,因此执行了这个两个命令之后的话,就没必要再执行unwatch了。
127.0.0.1:6379> unwatch OK
-
-
Redis事务三特性
-
单独的隔离操作
事务中的所有命令都会序列化,按顺序执行,事务在执行的过程中,不会被其他客户端发来的命令请求所打断。
-
没有隔离级别的概念
队列中的命令没有提交(exec)之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
-
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
-
8. Redis持久化
-
总体介绍
Redis是一个基于内存的数据库,它的数据是存放在内存中的,这就产生一个问题:掉电会丢失。
但Redis支持持久化,就是将数据写到硬盘中。
Redis提供了2种持久化方式:
- RDB(Redis DataBase)
- AOF(Append Only File)
8.1 RDB(Redis DataBase)
-
RDB是什么
RDB是在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
-
备份是如何执行的?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对数据恢复的完整性不是非常敏感,那么RDB方式要比AOF方式更加高效。
RDB的缺点是最后一次持久化后的数据可能丢失
-
Fork
- Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
- 在linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,linux引入了写时复制技术
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段内容要发生变化时,才会将父进程的内容复制一份给子进程
-
RDB持久化流程
-
RDB的相关配置
-
指定备份文件的名称
在redis.conf中,可以修改rdb备份文件的名称,默认为dump.rdb
-
指定备份文件保存的目录
在redis.conf中,rdb文件保存的路径,也可以修改,默认是在Redis启动时命令行所在目录下
-
触发RDB备份
-
自动备份,需要配置备份规则
在redis.conf中配置rdb备份规则,默认的规则如下:
默认是注释掉的,需要打开
默认的规则是:
3600秒(1小时)内修改了1次 或 300秒(5分钟)内修改100次 或 60秒(1分钟) 内修改了一万次
禁用RDB的方法:不设置save指令或者给save传入空字符串。
save命令用来配置备份的规则:
save 秒数 写操作次数
-
手动执行命令备份(save/bgsave)
有2个命令可以触发备份
save
:save时只管保存,其他不管,全部阻塞,手动保存,不建议使用bgsave
:redis会在后台异步进行快照操作,快照同时还可以响应客户端请求
可以通过
lastsave
命令获取最后一次成功生成快照的时间。
-
-
stop-writes-on-bgsave-error
: 当磁盘满时,是否关闭redis的写操作推荐yes
-
rdbcompression
:RDB备份是否开启压缩对于存储到磁盘中的快照,可以设置是否进行压缩存储,如果是的话,redis会采用LZF算法进行压缩。
如果不想消耗CPU来进行压缩的话,可以设置为关闭此功能,推荐设置为yes
-
rdbchecksum
:是否检查rdb备份文件的完整性在存储快照后,还可以让redis用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。推荐yes
-
-
RDB的备份和恢复
-
先通过
config get dir
查询rdb文件的目录127.0.0.1:6379> config get dir 1) "dir" 2) "/data"
-
然后将rdb的备份文件
*.rdb
拷贝到别的地方我们使用了docker,在启用redis时候映射
/data
目录到本地的F:\redis\data
所以实质上我们移不移动这个备份文件都可以,如果移走到别的目录,那么恢复时候(重新开启一个redis容器时,将
/data
映射目录映射到有备份文件的目录) -
rdb的恢复操作
-
关闭redis
直接停止docker容器,并且删除掉这个容器
C:\Users\zzz>docker ps # 查看当前正在运行的容器 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ad69cc999d4d redis:6.2.7 "docker-entrypoint.s…" 16 minutes ago Up 16 minutes 0.0.0.0:16379->6379/tcp redis1 C:\Users\zzz>docker stop ad69cc999d4d # 停止这个容器 ad69cc999d4d C:\Users\zzz>docker ps # 再次查看已经停止了 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES C:\Users\zzz>docker rm ad69cc999d4d # 删除这个容器 ad69cc999d4d
-
先把备份的文件拷贝到工作目录
这里在本地新建一个目录
data1
,将备份文件拷贝到这里 -
重新启动redis,备份数据直接加载,数据被恢复
C:\Users\zzz>docker run -p 16379:6379 --name redis1 -v F:\redis\data1:/data -v F:\redis\conf\redis.conf:/etc/redis/redis.conf -d redis:6.2.7 redis-server /etc/redis/redis.conf b70c0835ed178532df7076f6b526691748cc61b9917dd42ab9ad71488f970899 C:\Users\zzz>docker exec -it redis1 /bin/bash root@b70c0835ed17:/data# redis-cli 127.0.0.1:6379> keys * # 原先的key仍旧在 1) "m1" 2) "m2" 3) "zzy" 4) "k2" 5) "k1"
-
-
-
RDB的优势
- 适合大规模数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
-
RDB的劣势
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀,性能需要考虑
- 虽然redis在fork时使用了写时复制技术,但是如果数据庞大时还是比较消耗性能
- 在备份周期在一定间隔时间做一次备份,所以如果redis意外宕机的话,就会丢失最后一次快照后的所有修改
-
动态停止RDB
redis-cli config set save ""
save后面给空值,表示禁用保存策略。 -
小结
8.2 AOF(Append Only File)
-
是什么?
以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初回读取该文件重新构建数据,换言之,redis重启的话就可以根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
aof的优点:数据保证不丢失,自动缩小
aof的缺点:性能相对较差,体积相对较大,恢复速度慢
-
AOF持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区内
- AOF缓冲区会根据AOF持久化策略
[always,everysec,no]
将操作sync同步到磁盘的AOF文件中 - AOF文件大小超过重写策略或手动重写时,会对AOF文件进行重写(rewrite),压缩AOF文件容量
- redis服务器重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
-
AOF配置
可以在redis.conf中进行配置:
AOF默认不开启,通过配置项
appendony yes
开启AOF文件名称,默认为
appendonly.aof
AOF文件保存路径和RDB的路径一致(docker启动默认在
/data
目录) -
AOF和RDB同时开启,redis听谁的?
AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
-
AOF同步频率设置
AOF的同步频率在redis.conf文件中设置
-
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;
性能较差但数据完整性比较好
-
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失
-
appendfsync no
redis不主动进行同步,把同步时机交给操作系统
-
-
AOF启动/修复/恢复
AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作通RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
- 正常恢复
- 修改默认的
appendonly no
,改为yes
- 将有数据的aof文件复制一份保存到对应的目录(查看目录:
config get dir
) - 恢复:重启redis然后重新加载
- 修改默认的
- 异常恢复
- 修改默认的
appendonly no
,改为yes
- 如遇到aof文件损坏,可以通过
redis-check-aof --fix appendonly.aof
进行修复 - 恢复:重启redis然后重新加载
- 修改默认的
- 正常恢复
-
Rewrite压缩(AOF文件压缩)
-
Rewrite压缩是什么?
AOF采用文件追加方式,文件会越来越大,为了避免出现此情况,新增了重写机制,当AOF文件的大小超过锁审定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用
bgrewriteaof
触发重写 -
重写原理,如何实现重写?
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后在rename),redis4.0版本后的重写,是指把rdb的快照,以二进制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
no-appendfsync-on-rewrite
:该参数表示正在进行AOF重写时不会将AOF缓冲区中的数据同步到旧的AOF文件磁盘,也就是说在进行AOF重写的时候,如果此时有写操作进来,此时写操作的命令会放在aof_buf缓存中(内存中),而不会追加到原来的AOF文件中,这么做就是为了避免同时写原来的AOF文件和新的AOF文件对磁盘产生压力。
如果为yes,不写入aof文件,只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果为no,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞(数据安全,但是性能降低)
-
触发机制,何时重写?
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间,但是每次重写还是有一定的负担的,因此设定redis要满足一定条件才会进行重写
auto-aof-rewrite-percentage
:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)auto-aof-rewrite-min-size
:设置重写的基准值,最小文件64MB,达到这个值开始重写比如:文件达到了70MB开始重写,降到50MB,那下次什么时候开始重写?100MB的时候
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果redis的AOF当前大小>= base_size + base_size * 100%(
auto-aof-rewrite-percentage
默认值) 且当前大小>=64mb(auto-aof-rewrite-min-size
默认值)的情况下,Redis会对AOF进行重写 -
重写流程
bgrewriteaof
触发重写,判断是否当前有bgsave
或bgrewriteaof
在运行,如果有,则等待该命令结束后再继续执行。- 主进程fork出子进程执行重写操作,保证主进程不会阻塞
- 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失
- 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息;主进程把aof_rewrite_buf中的数据写入到新的AOF文件
- 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写
-
-
优势和劣势
- 优势
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文本,通过操作AOF文件,可以处理误操作
- 劣势
- 比RDB占用更多的磁盘空间
- 恢复备份速度要慢
- 每次读写都同步的话,有一定的性能压力
- 存在个别bug,造成不能恢复
- 优势
-
小结
- AOF文件是一个只进行追加的日志文件
- Redis可以在AOF文件体积变得过大时,自动地在后台对AOF文件进行重写
- AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松。
- 对于相同的数据集来说,AOF文件的体积通常要大于RDB文件的体积
- 根据所使用的fsync策略,AOF的速度可能会慢于RDB
8.3 总结
-
选哪个好?
官方推荐两个都启用
如果对数据不敏感,可以选择单独用RDB
不建议单独使用AOF,因为可能会出现bug
如果只是做纯内存缓存,可以都不用。
-
官网建议
-
RDB持久化方式能够在指定的时间间隔对你的数据进行快照存储
-
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始数据,AOF命令以redis协议朱家保存每次写的操作到AOF文件末尾
-
Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
-
只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式
-
同时开启两种持久化方式
-
在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整
-
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?
建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
-
性能建议
- 因为RDB文件只用做后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留
save 900 1
这条规则 - 如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单,只load自己的AOF文件就可以了
- AOF的代驾,一是带来持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的
- 只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64mb太小了,可以设置到
5G
以上 - 默认超过原大小的100%大小时重写可以改到适当的数值
- 因为RDB文件只用做后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留
-
9.Redis主从复制
-
是什么?
主机更新后根据配置和策略,自动同步到从机的
master/slave
机制;Master以写为主,Slave以读为主。 -
能干嘛?
- 读写分离,性能扩展,降低主服务器的压力
- 容灾,快速恢复,主服务器挂掉时,从机变为主机
-
怎么玩?
-
配置1主2从机器
这里咱们使用docker来搭建,因为我本地已经用来6379端口,因此三台docker容器在windows端口我分布如下(在容器内部端口都是6379):
角色 端口 Master 16379 slave1 16389 slave2 16399 -
主从配置Redis
-
在本地的conf目录创建三份conf文件
复制官方的配置文件,分别命名为
redis-16379.conf
,redis-16389.conf
,redis-16399.conf
-
在本地的data目录创建三个目录
分别为
master
,slave1
,slave2
-
docker配置容器自定义网络
为什么要配置容器自定义网络?是因为我用的是docker运行redis,如果不定义,每次重启docker容器后网络IP就会发生变化,会导致我的主从配置失败,可能从机log会有这样子的错误:
Master is currently unable to PSYNC but should be in the future: -NOMASTERLINK Can't SYNC while not connected with my master
,所以需要自定义网络-
docker network ls
首先查看docker的网络类型,默认是有三种的:
bridge(桥接网络),host(主机网络),none(无制定网络)
-
docker network create --subnet=172.10.0.0/16 redis-network
创建自定义网络
redis-network
,网段为172.10.0.0/16
-
-
修改主从配置文件内容
-
redis-16379.conf
# 1.修改允许所有ip访问,默认为:bind 127.0.0.1 -::1 bind 0.0.0.0 # 2. dir目录 dir ./master # 3. 打开AOF appendonly yes # 4. 主机设置密码,这里设置为了root requirepass root # 5. 修改repl-diskless-load 默认为disabled repl-diskless-load on-empty-db
repl-diskless-load这个配置是我在测试中出现了错误,报错:
Failed trying to load the MASTER synchronization DB from disk: No such file or directory
经过搜索后查找到这篇文章,尝试后可解决,参考文章https://www.cnblogs.com/emmith/p/16466809.html
-
redis-16389.conf
# 1.修改允许所有ip访问,默认为:bind 127.0.0.1 -::1 bind 0.0.0.0 # 2. dir目录 dir ./slave1 # 3. 打开AOF appendonly yes # 4. 主机设置密码,这里设置为了root requirepass slave1 # 5. 修改repl-diskless-load 默认为disabled repl-diskless-load on-empty-db # 6. 指定主机 replicaof 172.10.0.2 6379 # 7. 指定主机密码 masterauth root
replicaof 只有从机需要设置,主机不需要!
-
redis-16399.conf
# 1.修改允许所有ip访问,默认为:bind 127.0.0.1 -::1 bind 0.0.0.0 # 2. dir目录 dir ./slave2 # 3. 打开AOF appendonly yes # 4. 主机设置密码,这里设置为了root requirepass slave2 # 5. 修改repl-diskless-load 默认为disabled repl-diskless-load on-empty-db # 6. 指定主机 replicaof 172.10.0.2 6379 # 7. 指定主机密码 masterauth root
-
-
启动三个Redis容器,一主二从
docker run -p 16379:6379 --name master --net redis-network --ip 172.10.0.2 -v F:\redis\data:/data -v F:\redis\conf\redis-16379.conf:/etc/redis/redis.conf -d redis:6.2.7 redis-server /etc/redis/redis.conf docker run -p 16389:6379 --name slave1 --net redis-network --ip 172.10.0.3 -v F:\redis\data:/data -v F:\redis\conf\redis-16389.conf:/etc/redis/redis.conf -d redis:6.2.7 redis-server /etc/redis/redis.conf docker run -p 16399:6379 --name slave2 --net redis-network --ip 172.10.0.4 -v F:\redis\data:/data -v F:\redis\conf\redis-16399.conf:/etc/redis/redis.conf -d redis:6.2.7 redis-server /etc/redis/redis.conf
这里的
--net redis-network
是指定使用我们的自定义网络,--ip
指定容器的ip可以看到,三个容器都运行起来了:
-
登录容器,查看主从复制的相关信息
命令:
info replication
Master:
Slave2:
Slave1:
-
验证主从复制效果
在master上执行写命令,slave上读取
-
-
-
主从复制原理
- slave启动成功连接到master后会发送一个
sync
命令 - Master接到命令启动后台存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:Master继续将新的所有收集到的修改命令一次传给slave,完成同步
- 但是只要重新连接master,一次完全同步(全量复制)将被自动执行
- slave启动成功连接到master后会发送一个
-
小结
-
主机redis挂掉后情况如何?从机是上位还是原地待命?
主机挂掉后,从机会待命,小弟始终是小弟,会等着大哥恢复,不会上位
-
从机挂掉后又恢复了,会继续从主机同步数据么?
会的,当从机重启后,会继续将中间缺失的数据同步过来
-
-
薪火相传模式
上一个slave可以是下一个slave的Master,slave同样可以接收其他slave的连接和同步请求,那么该slave作为了链条中下一个master,可以有效减轻master的写压力,去中心化降低风险。
若中途变更转向:会清除之前的数据,重新建立拷贝最新的。
风险是:一旦某个slave挂机了,后面的slave都没法备份。
主机挂了,从机还是从机,无法写数据
-
反客为主模式
当一个master挂机后,可以选择一个slave作为主机。
如果我们想让slave1作为主机,那么可以在slave上执行以下命令:
slaveof no one
此时slave1就变成了主机了,然后再去其他slave中执行
slaveof masterip masterPort
将其挂到新的主机slave1中,如果主机有密码,还需要使用命令设置config set masterauth xxx
.这种反客为主模式有个缺点:需要手动执行命令,很不方便
需要使用另一种方式:哨兵模式,在主机挂掉后,能自动从slave中选举一个作为主机,自动实现故障转转移。
10. 哨兵模式(Sentinel)
-
是什么?
反客为主模式的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
-
原理
sentinel会根据指定的频率给master发送ping请求,看看master是否还活着,如果master在指定时间内未正常响应sentinel发送的ping请求,sentinel则认为master挂掉了,但是这种情况存在误判的可能,比如可能master并没有挂掉,只是他们的网络不通导致ping失败。
为了避免误判,通常会启动多个sentinel,一般是奇数个,比如3个,那么可以指定当有多个sentinel都觉得master挂掉了,此时才判断master是真的挂掉了,通常这个值设为sentinel数量的一半,比如sentinel数量是3个,那么可以设置这个值为2个。
当多个sentinel判定master已经挂掉了,接下来sentinel会进行故障转移:从slave中投票选出一个服务器,将其升级为新的主服务器,并让失效主服务器的其他从服务器slaveof指向新的主服务器;当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
-
怎么玩?
-
配置1主2从3哨兵
1主2从沿用上面主从复制的设置,在此基础增加三个哨兵,分别如下
名字 端口 sentinel1 26379 sentinel2 26389 sentinel3 26399 -
先将1主2从启动起来
特别需要注意:前面三个容器的密码分别为root,slave1,slave2,是不一致的,在哨兵模式下需要配置为一样的,否则在主机宕机后,哨兵无法转移.
这里所有容器修改配置文件:
requirepass root
,masterauth root
然后将上面主从复制的3个docker容器restart
-
在本地目录新建一个log目录,并新建三个目录:
sentinel26379
,sentinel26389
,sentinel26399
-
然后在本地conf目录创建sentinel的配置文件
sentinel-26379.conf
,sentinel-26389.conf
,sentinel-26399.conf
port 26379 dir "/var/log/sentinel" logfile "/var/log/sentinel/sentinel-26379.log" # 监控主节点名字 ip 端口,2代表是有多少个哨兵认为主机挂了 sentinel monitor master 172.10.0.2 6379 2 # ping超时时间(单位毫秒) sentinel down-after-milliseconds master 5000 # 当 Sentinel 节点集合对主节点故障判定达成一致时,Sentinel 领导者节点会做故障转移操作,选出新的主节点, # 原来的从节点会向新的主节点发起复制操作,parallel-syncs 就是用来限制在一次故障转移之后, # 每次向新的主节点发起复制操作的从节点个数,指出 Sentinel 属于并发还是串行。 # 1代表每次只能复制一个,可以减轻 Master 的压力; sentinel parallel-syncs master 1 # 表示故障转移的时间 sentinel failover-timeout master 180000 # 主节点密码 sentinel auth-pass master root
port 26389 dir "/var/log/sentinel" logfile "/var/log/sentinel/sentinel-26389.log" # 监控主节点名字 ip 端口,2代表是有多少个哨兵认为主机挂了 sentinel monitor master 172.10.0.2 6379 2 # ping超时时间(单位毫秒) sentinel down-after-milliseconds master 5000 # 当 Sentinel 节点集合对主节点故障判定达成一致时,Sentinel 领导者节点会做故障转移操作,选出新的主节点, # 原来的从节点会向新的主节点发起复制操作,parallel-syncs 就是用来限制在一次故障转移之后, # 每次向新的主节点发起复制操作的从节点个数,指出 Sentinel 属于并发还是串行。 # 1代表每次只能复制一个,可以减轻 Master 的压力; sentinel parallel-syncs master 1 # 表示故障转移的时间 sentinel failover-timeout master 180000 # 主节点密码 sentinel auth-pass master root
port 26399 dir "/var/log/sentinel" logfile "/var/log/sentinel/sentinel-26399.log" # 监控主节点名字 ip 端口,2代表是有多少个哨兵认为主机挂了 sentinel monitor master 172.10.0.2 6379 2 # ping超时时间(单位毫秒) sentinel down-after-milliseconds master 5000 # 当 Sentinel 节点集合对主节点故障判定达成一致时,Sentinel 领导者节点会做故障转移操作,选出新的主节点, # 原来的从节点会向新的主节点发起复制操作,parallel-syncs 就是用来限制在一次故障转移之后, # 每次向新的主节点发起复制操作的从节点个数,指出 Sentinel 属于并发还是串行。 # 1代表每次只能复制一个,可以减轻 Master 的压力; sentinel parallel-syncs master 1 # 表示故障转移的时间 sentinel failover-timeout master 180000 # 主节点密码 sentinel auth-pass master root
-
启动哨兵
docker run -d --name sentinel-26379 -v F:\redis\conf\sentinel-26379.conf:/conf/sentinel.conf -v F:\redis\log\sentinel26379:/var/log/sentinel --network redis-network redis:6.2.7 redis-sentinel /conf/sentinel.conf docker run -d --name sentinel-26389 -v F:\redis\conf\sentinel-26389.conf:/conf/sentinel.conf -v F:\redis\log\sentinel26389:/var/log/sentinel --network redis-network redis:6.2.7 redis-sentinel /conf/sentinel.conf docker run -d --name sentinel-26399 -v F:\redis\conf\sentinel-26399.conf:/conf/sentinel.conf -v F:\redis\log\sentinel26399:/var/log/sentinel --network redis-network redis:6.2.7 redis-sentinel /conf/sentinel.conf
这里与启动redis多映射了一个log日志目录,还有启动时使用了
redis-sentinel
指定sentinel配置文件启动。 -
查看sentinel的信息
先进入docker容器:
docker exec -it sentinel-26379 /bin/bash
然后连接redis,指定端口(因为端口已经不再是默认的6379了)
redis-cli -p 26379
使用命令
info sentinel
查看信息 -
验证故障自动转移
-
首先关闭master容器
docker stop master
-
等待10s后(配置文件中
down-after-milliseconds
值为5000,表示5秒,稍微等待长一点点),查看sentinel的log -
再去slave容器中查看主从复制信息
-
验证下slave1和slave2是否同步
slave1中设置name
127.0.0.1:6379> set name slave1_is_master OK
slave2中读取name
127.0.0.1:6379> get name "slave1_is_master"
-
-
-
有个问题:当之前的master机器重新启动后,会发生什么情况?是重新成为master吗?
结论:当旧的master恢复后,会挂在新的master下面。(其实上面sentinel的log已经显示了两个slave,分别是172.10.0.2,172.10.04)
-
将旧的master容器重新启动
docker restart master
-
进入容器,查看主从信息
C:\Users\zzz>docker exec -it master /bin/bash root@2c4170098e64:/data# redis-cli -a root Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 127.0.0.1:6379> info replication # Replication role:slave master_host:172.10.0.3 master_port:6379 master_link_status:up master_last_io_seconds_ago:1 master_sync_in_progress:0 slave_read_repl_offset:1881036 slave_repl_offset:1881036 slave_priority:100 slave_read_only:1 replica_announced:1 connected_slaves:0 master_failover_state:no-failover master_replid:aa3ecaaf1d1744ba063d0f13efef99fda9b957b7 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:1881036 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1868029 repl_backlog_histlen:13008
-
重新查看新的主节点slave1的主从信息
127.0.0.1:6379> info replication # Replication role:master connected_slaves:2 slave0:ip=172.10.0.4,port=6379,state=online,offset=1888141,lag=0 slave1:ip=172.10.0.2,port=6379,state=online,offset=1888141,lag=0 master_failover_state:no-failover master_replid:aa3ecaaf1d1744ba063d0f13efef99fda9b957b7 master_replid2:d11651a4dc25f5608f5a9f084356063aa90485ea master_repl_offset:1888274 second_repl_offset:10650 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:839699 repl_backlog_histlen:1048576
-
可以看到,确实是旧的主节点作为从机加入到新的主节点下
-
-
哨兵模式选择新的主节点的条件顺序
-
选择优先级靠前的
在redis.conf中有一项配置:
replica-priority 100
,默认值100,值越小优先级越高 -
选择偏移量最大的
偏移量是指获得原主机数据最全的
-
选择runid最小的从服务
每个redis实例启动后都会随机生成一个40位的runid,使用
info server
可以查看到runid
-
11. Redis集群
-
存在的问题
- 单台redis容量限制,如何进行扩容?
- 单台redis并发写量太大有性能瓶颈,如何解决?
之前通过代理主机来解决,但是redis3.0开始提供了集群可以解决这些问题
-
什么是集群?
前面的主从模式+哨兵模式结合起来使用以及可以满足大部分的redis高可用场景了,但是它存在一个很明显的缺点:就是只有一台master对外提供写服务,对并发写量很大的情况无法缓解压力。因此Redis3.0版本开始引入了redis集群。
Redis集群实现了对redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
为了保证集群的高可用,每个master节点下需要添加至少一个slave节点,这样当某个master节点发生故障后,可以从它的slave节点中选一个作为新的master继续提供服务;但是当master和它下面的所有slave节点都挂了时,整个集群就不可用了。
-
集群中的哈希槽
Redis集群中引入了哈希槽的概念,Redis集群内部划分了有2^14=16384个哈希槽,当需要在Redis集群中放置一个key-value时,redis会对key进行一个计算:
crc16(key) % 16384
,得出这个key的哈希槽值,然后根据哈希槽的分布,将这个节点保存到对应的机器上。(其实就是类似于分库)比如有三台master,master1负责0-5460号哈希槽,master2负责5461-10922号哈希槽,master3负责10923号-16383号哈希槽,假设key的哈希槽值为1024,那么这个key就会分配保存到master1节点上。
-
docker搭建redis集群(3主3从)
下面使用Docker搭建一个3主3从的集群,每个主节点下挂一个slave节点。
-
首先在
F:\redis
目录中新建一个cluster
目录,在cluster
目录下分别新建conf
,data
,log
目录在
conf
目录中将redis.conf
官方配置文件复制六份,分别命名为redis-master1.conf
,redis-master2.conf
,redis-master3.conf
,redis-slave1.conf
,redis-slave2.conf
,redis-slave3.conf
在
data
和log
目录中分别创建master1
,master2
,master3
,slave1
,slave2
,slave3
六个目录 -
修改配置文件内容(6个配置文件修改一样,只列出一个)
# 修改bind ip地址 bind 0.0.0.0 # 设置密码 requirepass 123456 # 因为配置了密码,而且是集群,主从,因此还需设置masterauth masterauth 123456 # 设置日志路径 logfile "/var/log/redis/redis-server.log" # 配置集群相关信息,将注释去掉 cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 15000 # 修改repl-diskless-load 默认为disabled repl-diskless-load on-empty-db
windows使用docker部署,需要将这个配置项
repl-diskless-load
设置为on-empty-db
-
启动6个容器,命名为master1,master2,master3,slave1,slave2,slave3
docker run -d --name master1 -v F:\redis\cluster\conf\redis-master1.conf:/conf/redis.conf -v F:\redis\cluster\data\master1:/data -v F:\redis\cluster\log\master1:/var/log/redis --network redis-network redis:6.2.7 redis-server /conf/redis.conf docker run -d --name master2 -v F:\redis\cluster\conf\redis-master2.conf:/conf/redis.conf -v F:\redis\cluster\data\master2:/data -v F:\redis\cluster\log\master2:/var/log/redis --network redis-network redis:6.2.7 redis-server /conf/redis.conf docker run -d --name master3 -v F:\redis\cluster\conf\redis-master3.conf:/conf/redis.conf -v F:\redis\cluster\data\master3:/data -v F:\redis\cluster\log\master3:/var/log/redis --network redis-network redis:6.2.7 redis-server /conf/redis.conf docker run -d --name slave1 -v F:\redis\cluster\conf\redis-slave1.conf:/conf/redis.conf -v F:\redis\cluster\data\slave1:/data -v F:\redis\cluster\log\slave1:/var/log/redis --network redis-network redis:6.2.7 redis-server /conf/redis.conf docker run -d --name slave2 -v F:\redis\cluster\conf\redis-slave2.conf:/conf/redis.conf -v F:\redis\cluster\data\slave2:/data -v F:\redis\cluster\log\slave2:/var/log/redis --network redis-network redis:6.2.7 redis-server /conf/redis.conf docker run -d --name slave3 -v F:\redis\cluster\conf\redis-slave3.conf:/conf/redis.conf -v F:\redis\cluster\data\slave3:/data -v F:\redis\cluster\log\slave3:/var/log/redis --network redis-network redis:6.2.7 redis-server /conf/redis.conf
-
查看下docker容器是否运行起来了,以及node-6379.conf文件是否生成(在data目录下)
-
将6个容器节点组合成一个集群
随意进入一个容器,执行以下命令:
redis-cli -p 6379 -a 123456 --cluster create 172.10.0.2:6379 172.10.0.3:6379 172.10.0.4:6379 172.10.0.5:6379 172.10.0.6:6379 172.10.0.7:6379 --cluster-replicas 1
- 因为集群使用了密码,所以需要
-a
指定密码 --cluster create
后跟上所有节点的ip:port
,以空格隔开--cluster-replicas
:为1 表示采用最简单的方式配置集群,也就是每个master配1个slave- docker容器的ip获取:
docker inspect 容器名 | findstr IPAddress
(windows的cmd窗口使用findstr)
执行过程如下:
中间会询问我们是否同意这样的分配方式,输入yes
最后显示成功,16384个哈希槽也被分配好了。
master1:0-5460号
master2:5461-10922
master3:10923-16383
- 因为集群使用了密码,所以需要
-
查看集群状态信息
随意进入一个节点,使用命令
cluster nodes
查看在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间
-
测试集群
登入master2容器,设置key
这里需要加上
-c
,表示使用集群策略连接,这样子当设置数据时候会自动切换到写的主机看起来好像和之前的没啥不同,实际上是因为这两个key的哈希槽刚好是落在master的范围内,所以设置和查询时候都是直接操作master2,下面看看使用其他节点来查询key和设置key
需要注意:
从上面获取的ip可知主从对应关系为:
master1(172.10.0.2) -> slave2(172.10.0.6)
master2(172.10.0.3) -> slave3(172.10.0.7)
master3(172.10.0.4) -> slave1(172.10.0.5)
使用slave1节点进行查询和设置key:
在redis的官方文档中,对redis-cluster架构上,有这样的说明:在cluster架构下,默认的,一般redis-master用于接收读写,而redis-slave则用于备份,当有请求是在向slave发起时,会直接重定向到对应key所在的master来处理。 但如果不介意读取的是redis-cluster中有可能过期的数据并且对写请求不感兴趣时,则亦可通过readonly命令,将slave设置成可读,然后通过slave获取相关的key,达到读写分离
-
-
Redis集群如何分配这个这6个节点?
一个集群中至少要有3个主节点,因为新的master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新的master的条件的。
分配原则尽量保证每个主库运行在不同的ip,每个主库和从库不在一个ip上,这样才能做到高可用。
-
在集群中录入值
不在同一个slot下的键值,是不能使用mget,mset等多键操作的。redis集群只支持所有key落在同一slot的情况
172.10.0.4:6379> mset k1 v1 k2 v2 k3 v3 (error) CROSSSLOT Keys in request don't hash to the same slot
可以通过
{}
来定义组的概念,这样哈希槽计算的只会是{}
里的值,就可以保证不同(哈希槽值)的key落在同一个slot里了。172.10.0.4:6379> mset k1{cust} v1 k2{cust} v2 k3{cust} v3 -> Redirected to slot [4847] located at 172.10.0.2:6379 OK 172.10.0.2:6379> mget k1 k2 k3 (error) CROSSSLOT Keys in request don't hash to the same slot 172.10.0.2:6379> mget k1{cust} k2{cust} k3{cust} 1) "v1" 2) "v2" 3) "v3"
-
slot相关的一些命令
cluster keyslot <key>
:计算key对应的slotcluster countkeysinslot <slot>
:获取slot槽位中key的个数cluster getkeysinslot <slot> <count>
:返回count个slot槽中的键
-
故障恢复
-
如果主节点下线,从节点是否能够提升为主节点?
需要等待15秒,因为配置文件配置了
cluster-node-timeout 15000
将master1停掉,看看情况
docker stop master1
maste1下的从节点slave2,变成了master了
-
原先的主节点master1又重新恢复了,那主从关系会如何?
将master1容器重新启动:
docker start master1
结果如下:master1恢复后成为了slave2下的从节点
-
如果某一段slot的主从都宕机了,redis服务是否还能继续?
这个时候要看配置项
cluster-require-full-coverage
参数的值了yes(默认值)
:整个集群都挂掉了no
:宕机的这部分的槽位数据全部不能使用,其他槽位正常
-
-
小结
- redis集群提供了以下好处
- 实现扩容
- 分摊压力
- 无中心噢诶之相对简单
- redis集群的不足
- 多键操作是不被支持的,必须在一个槽里
- redis事务操作的key必须在一个节点上
- 只有一个数据库db0
- 维护不容易
集群不一定是最优选,应该根据不同的业务需求来判断是否需要搭建集群,当较小的业务使用单机redis以及redis哨兵模式就能满足需求时,不应该强行搭建集群。
- redis集群提供了以下好处
12.Redis应用问题解决
-
缓存穿透
-
问题描述
当系统使用了redis作为缓存后,每个请求会先从redis缓存中查询,缓存中有就直接返回,没有就去数据库查询,数据库中存在则返回并放到缓存中,但是有些key对应的数据在数据库中并不存在,然后每次针对此key的请求从缓存中获取不到,请求就会压到数据库,高并发下就可能压垮数据库。
比如一个不存在的用户id获取用户信息,无论缓存还是数据库都没有这个数据,若黑客利用此漏洞大量攻击可能压垮数据库
-
解决方案
-
对空值缓存
如果一个查询返回的数据为空(不管数据是否存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过5分钟
-
设置可访问的名单(白名单)
使用redis的bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问
-
采用布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的,它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检查一个元素是否在一个集合中,它的优点是空间效率和查询的时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
-
进行实时监控
当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
-
-
-
缓存击穿
-
问题描述
redis中某个key对应的数据存在,但在redis中过期,此时有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并返回设置到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存击穿出现的现象:
- 数据库访问压力瞬时增大
- redis里面没有出现大量的key过期
- redis正常运行
-
解决方案
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据,这个时候需要考虑一个问题:缓存被“击穿”的问题,常见解决方案如下:
-
预先设置热门数据
在redis高峰访问之前,把一些热门的数据提前存入到redis里面,加大这些热门数据key的时长
-
实时调整
现场监控哪些数据热门,实时调整key的过期时长
-
使用锁
- 就是在缓存失效的时候(判断拿出来的值为空),不是立即去数据库查询
- 而是先使用缓存工具的某些带成功操作返回值的操作(比如redis的setnx)去set一个mutex key
- 当操作返回成功时,再进行数据库查询,并回设缓存,最后删除mutex key
- 当操作返回失败,证明有线程在查询数据库,当前线程休眠一段时间后再重试整个get缓存的方法。
-
-
-
缓存雪崩
-
问题描述
key对应的数据存在,但在redis中过期,若此时有大量并发请求过来,发现缓存中数据过期,大量的请求就可能会瞬间压垮后端db
缓存雪崩与缓存击穿的区别在于雪崩针对很多key缓存,击穿则是某一个key
-
解决方案
-
构建多级缓存架构
nginx缓存+redis缓存+其他缓存(ehcache等)
-
使用锁或队列
用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量并发请求落到底层存储系统上,不适用高并发情况。
-
设置过期标志更新缓存
记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存
-
将缓存失效时间分散开
比如我们可以在原有的失效时间上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
-
-
-
分布式锁
-
问题描述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程,多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
-
分布式锁主流的实现方案
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点
- 性能:redis最高
- 可靠性:zookeeper最高
-
使用redis实现分布式锁
-
需要使用下面命令实现分布式锁
set key value NX PX 有效期(毫秒)
这条命令表示:当key不存在时候,才可以设置值为value,而且设置过期时间
-
上锁过程
过程如下图,执行命令
set key value NX PX 有效期
,返回ok表示执行成功,则获取锁成功,多个客户端并发执行此命令的时候,redis确保只有一个可以执行成功。- 多个客户端同时获取锁(sexnx)
- 获取成功,执行业务路基(从db获取数据,放入缓存),执行完成释放锁(del)
- 其他客户端等待重试
-
优化:设置锁的过期时间
上面使用锁的方式存在一个问题,当客户端刚好获取到锁之后,业务逻辑出现异常,导致锁无法释放。因此需要在设置锁的同时设置一个过期时间,使之能自动释放锁。
-
解决锁误删问题
可能存在客户端B释放了客户端A的锁。
如下场景:如果业务逻辑执行时间是7s,执行流程如下
- A业务逻辑没执行完,3秒后所被自动释放
- B获取到锁,执行业务,3秒后所被自动释放
- C获取到锁,执行业务逻辑
- A业务逻辑执行完成,开始调用del释放锁,这时候锁是C的,被A给释放了,等于C没锁
解决方法:
**setnx获取锁时候,vaule设置为一个全局的唯一值(比如uuid),释放前获取这个值和本地的比较,判断是否是自己的锁**
-
优化:解决UUID防误删
问题:删除锁操作缺乏原子性
场景如下:
- A执行删除时候,查询到uuid值确实本地uuid相等
- A执行删除前,锁刚好过期时间到,被redis自动释放
- B获取到了锁,开始执行
- A执行删除,此时会把B的锁给删除了(因为他已经查询过uuid值和本地一致)
根本原因:判断锁的value是不是全局唯一值和删除锁这2个步骤对redis来说都不是原子操作,无法保证原子性
-
终极解决方案:使用Lua脚本来释放锁
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。
Lua脚本类似于redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务的操作。
但是需要注意redis的lua脚本功能,只能在redis2.6以上版本才可以使用。
-
-
小结
为了确保分布式锁可用,我们至少需要确保分布式锁的实现同时满足以下4个条件:
- 互斥性,在任意时刻只能有一个客户端能够持有锁
- 不会发生死锁,即使有一个客户端在持有锁期间崩溃而没有释放锁,也能够保证后续其他客户端能够加锁
- 解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端不能把别人的锁给解了
- 加锁和解锁必须有原子性
学习文章:Redis常见问题及解决方案
-