目录
一、解决redis服务器端口问题
redis客户端在windows环境下(IDEA),redis在linux云服务下,若客户端想访问服务器,首先需要在云服务器外网IP下访问,其次还需要能访问到服务器端口。修改IP后,如何访问到服务器端口???redis的服务器端口默认是被云服务器的防火墙保护着的,即使在外网ip下也是不能访问的。那需要将redis服务器的端口号像tomcat一样,设成开放形式吗???这种做法是不可行的,tomcat即使设成开放形式,黑客也不容易入侵,但若redis设成开放形式,是很容易被黑客入侵的。那如何在不开放端口的情况下,访问到redis服务器???
解决方法:
1、将java代码打包成可执行的jar包,直接在linux服务器上运行。
2、配置ssh端口转发,把云服务器的redis端口,映射到本地主机
ssh支持端口转发,可以将redis服务器端口6379映射到本地端口8888,后续可以直接访问:127.0.0.1:8888就可以访问本机的redis服务器了。
在linux云服务器上进行配置:
检查是否配置成功,输入netstat查看本地是否有8888端口:
ps:当配置了端口转发后,一定要断开连接进行重新连接。
二、java环境下使用redis
1、引入依赖
2、使用(这里就只拿String举例,语法基本和redis命令一样)
(1)get和set
(2)mget和mset
(3)incr和decr
三、javaSpringt环境下使用redis
1、配置文件,连接redis
2、选择redis
3、使用
springboot对redis不同类型进行了封装,可以使用不同类型下的一些方法。
四、redis持久化
1、持久化概念
持久化是指将数据保存在持久存储介质(如硬盘、数据库等),保证数据的长期存储,以便在需要时能够重新读取和处理数据。
2、redis持久化策略
redis将数据放在内存中,内存的特点是访问速度快,但是不能长期存储数据,当计算机关闭或重启时,内存中的数据就会被清空,如何使得redis能持久化存储数据???redis采用RDB和AOF两种策略,将数据同步到硬盘中,当redis重启时,用来恢复数据,使得redis具有持久化。
3、RDB策略
(1)实现原理
触发RDB持久化后,将redis内存中的所有数据生成快照(RDB二进制文件),保存到硬盘。
(2)触发RDB持久化
①手动触发
在redis客户端,执行特定的命令,触发rdb持久化,生成快照。
save命令:阻塞当前redis服务器,无法处理客户端的其他命令,全力以赴的进行快照生成操作。
bgsave命令:redis进程执行fork操作创建子进程,由子进程负责进行快照生成操作,完成后自动结束,redis服务器阻塞只发生在fork阶段。
bgsave执行流程:
当执行bgsave命令时,redis父进程先判断当前是否存在其他正在执行的子进程,如RDB/AOF子进程,如果存在bgsave直接返回;如果没有父进程执行fork操作创建子进程,子进程根据父进程内存数据生成临时快照文件,完成后对原有RDB文件进行替换,并且该进程发送信号给父进程表示完成,父进程更新统计信息。
举例:
此时就会手动写入rdb文件中。rdb文件保存在配置指定的目录,默认为(/var/lib/redis)下,文件名默认为dump.rdb。
假如没有手动写入硬盘,也没有达到自动触发的条件,若重新启动redis服务器,此时会获取到之前的数据吗???
若是通过正常流程启动redis服务器,此时redis在退出的时候会自动触发生成rdb文件;但若是异常重启(kill-9或者服务器掉电),此时redis来不及生成rdb文件,内存中的尚未保存到快照中的数据就会随着重启而丢失。
正常关闭:
异常关闭:
②自动触发
redis也可以自动触发rdb持久化机制。
使用save配置,如:"save m n "表示m秒内数据集发生了n次修改,自动触发rdb持久化机制;
正常关闭redis服务器时,也会自动触发rdb持久化机制;
redis进行主从复制时,主节点也会自动生成rdb快照,然后将rdb快照文件内容传输给从节点。
以上可以改变自动触发条件。
ps:如果修改了rdb文件内容,并且通过kill -9 关闭进程,则很可能重新启动redis时失败。当rdb文件损坏时,可以使用redis提供的redis-check-dump工具检查rdb文件并获取对应的错误报告。
(3)RDB策略的优缺点
优点:rdb是二进制文件,加载rdb恢复数据较快。
缺点:rdb策略不能实时更新数据到rdb文件,若此时redis服务器挂了,且内存中最新的数据还未更新到rdb文件中,此时这部分数据就丢失了;创建子进程,进行全量数据保存,属于重量级操作,频繁执行成本过高。
4、AOF策略
(1)实现原理
AOF是实时进行持久化的,重启redis时执行AOF文件中的命令达到恢复数据的目的。
所有的写入命令会追加到aof缓存区中,根据缓存区的刷新策略将数据更新到硬盘中(AOF文件),随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩目的。当redis服务器进行重启时,可以加载AOF文件进行数据恢复。
(2)AOF文件
首先需要开启aof,当开启aof时,rdb就不再生效了,redis服务器重启时,就不读取rdb文件了。
进入配置文件开启aof:
写入键值对,查看aof文件(路径和rdb一样):
(3)刷新策略
前面提到所有的写入命令会先放到aof缓冲区中,根据刷新策略再将数据更新到硬盘中,从而减少了操作硬盘的次数,提高效率。
刷新策略在配置文件中:
always:命令写入缓冲区就向硬盘中写入,写入频率最高,数据可靠性最高,性能最低;
everysec:同步线程每秒刷新一次缓冲区的数据到硬盘中,写入频率较低,数据可靠性较低,性能较高;
no:redis不会主动刷新缓冲区数据到硬盘中,而是直接交给操作系统去判断,写入频率最低,数据可靠性最低,性能最高。
(4)AOF文件重写
前面提到,当aof文件越来越大时,需要定期对aof文件进行重写,达到压缩的目的。
例如:set key1 111; set key2 222; set key1 222;
最后的结果应该只有两条,但如果不重写,aof会保存3条数据,此时就要求要定期对aof文件进行重写,压缩文件。
aof文件重写原理:
ps:fork之后的写入命令还需要写到缓冲区和旧aof文件,是为了防止新aof文件突然挂了,此时aof文件就没有fork之后的数据了,数据不准确。
(5)AOF策略优缺点
优点:aof策略是实时更新数据到aof文件,数据可靠性高;
缺点:aof文件是文本文件,加载aof恢复数据较慢。
5、混合持久化策略
redis结合了aof和rdb的特点,引入了混合持久化策略,每一个写入命令按照aof的方式(文本)写入文件,当触发重写时,将缓冲区所有内容以rdb(二进制)写入新文件中,使用新文件替换旧文件。后续进行写入命令时,还是按照aof方式写入文件。
五、redis事务
1、数据库事务
了解redis事务前,先回顾数据库事务。
数据库事务的概念:一组操作,要么全部成功,要么全部失败。
数据库事务的特点:①原子性;②一致性;③持久性;④隔离性;
2、redis事务特点
redis事务和数据库事务一样,都是对一组操作进行打包完成。
特点(数据库事务对比):①弱化的原子性;redis没有回滚机制,一组操作中若有一个命令执行失败,这一组操作还是会继续执行,不要求全部失败;②不保证一致性;redis不涉及约束,不保证结果都是合理有效的;③不需要隔离性;redis是单线程处理请求;④不具备持久性;redis数据存储在内存中不具有持久性,aof和rdb持久策略和事务无关。
3、redis事务的作用
当面对不同的redis客户端时,保证服务器对同一个客户端的一组操作全部执行完毕后,再执行下一个客户端的命令。
4、redis事务操作
(1)multi
开启一个事务,执行成功返回ok。后面的每个命令会先放在客户端队列里。
(2)exec
执行一个事务。
(3)discard
放弃当前事务,清空队列。
(4)watch
在执行事务时,假设客户端1先修改了key1的值为100,然后客户端2修改了key1的值为200,按照时间先后顺序,key1的值应为200,但若exec命令是在客户端2之后,此时key1的值就为100了,此时这种情况就会造成歧义。
使用watch命令监督某个key,若在执行事务时,其他客户端修改了该key,真正提交事务时服务器会发现监督的key版本号已经超过了事务开始时的版本号,就会让事务执行失败,所有命令都不执行。
(5)unwatch
取消对key的监控。
六、主从复制
1、主从模式
(1)作用
主从模式是分布式系统中的一种模式。当只有一个服务器来存储redis数据时,此时若该服务器挂了,后续客户端也无法从redis中读取数据了。在分布式系统中,希望有多个服务器来部署redis服务,构成一个redis集群。即使其中一个挂了,客户端还可以去其他服务器上读取数据,对于读操作的并发量下的可用性进行了提高。
(2)原理
主从模式中,有n个节点,其中有一个主节点,n-1个从节点。主节点可以修改数据,可以读数据;从节点只能读数据;当主节点数据发生变化时,从节点数据也需要一起变化;从节点挂了,不影响redis的使用;主节点挂了,此时就不能修改redis数据了。
2、主从复制
参与复制的redis服务器被划分为主节点和从节点,一个redis服务器只能有一个主节点,但可以拥有多个从节点,复制只能由主节点到从节点。
分布式系统中,每个服务器应该处于不同的云服务器上,但此时博主手里就一个云服务器上,所以就只能启动几个端口不同的redis服务器进行演示。
(1)启动三个不同端口的redis
启动3个不同的redis服务器,端口号分别为:6380、6381、6382。
端口号的设置我们在配置文件中修改,先建立一个目录,里面装3个不同端口redis的配置文件:
修改三个文件的端口号以及开启后台模式:
启动三个不同的redis:
(2)建立主从关系
以6380为主节点,6381和6382为从节点,修改从节点配置文件,建立主从关系;
同时需要修改每个节点aof所处的路径,不然会导致每个redis的aof文件是一样的:
以上三个用来放置每个redis服务器对应的aof文件。
修改aof文件的放置路径:
需注意:在修改了配置文件之后,需要重新启动redis.
如果使用命令redis-server+配置文件启动的redis,需要使用kill -p id杀掉redis进程;
如果使用命令service redis-server start这种方式启动的redis,需要使用service redis-server stop杀掉redis进程。
杀掉并重启从节点redis:
重启之后,每个redis就有自己的aof文件了。
(3)客户端与服务器之间建立连接
(4)效果演示
主节点建立key:
从节点获取key:
从节点建立key(失败):
(5)查看节点的状态
主节点:
offset:主节点会不断收到数据,从节点也要同步数据,但这个同步不是瞬间完成的,offset就记录了从节点同步数据的进度。
从节点:
(6)断开连接
主节点与从节点之间也可以断开连接。
例如:断开6380端口对应的redis连接,从节点变为主节点
(7)拓扑
redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为:一主一从、一主多从、树状主从结构。
一主一从:
一主多从:
树状主从结构:
(8)数据同步psync
redis使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。
全量复制:一般用于初次复制场景,会把主节点数据一次性全部发送给从节点;
部分复制:由于网络原因导致主从复制数据丢失,从节点再次连接上主节点后,若缺失的数据不多,主节点会部分复制数据给从节点。
实时复制:主从建立连接后,主节点会把自己收到的修改操作,通过tcp长连接,源源不断的传输给从节点,从节点就会根据这些请求修改自身的数据,保证主从节点数据一致。同时,主从节点也都有心跳检测机制,主节点每隔10s对从节点发生ping命令,判断从节点是否存活;从节点每隔1s向主节点发送命令告诉主节点当前复制偏移量。60s后没有响应,判断从节点下线,断开连接,从节点恢复连接后,心跳机制继续进行。
psync语法格式:psync replicationid offset
replicationid:指主节点的复制id,主节点启动或者从节点晋升为主节点都会生成一个replicationid
offset:偏移量,主节点每次在进行写入命令后,其偏移量会累加;从节点每次在复制主节点的数据后,其偏移量也会累加;当主节点的偏移量与主节点的偏移量相等时,此时主从节点数据一样。
注:当replicationid默认值为?,offset的默认值为-1时,此时为全量复制;
(9)psync运行流程
①初次复制,从节点发生psync命令给主节点,replid默认值为?,offset默认值为-1;
②主节点根据psync参数和自身数据情况决定响应结果:
如果回复+FULLRESYNC,则从节点需要进行全量复制;
如果回复+CONTINEU,从节点进行部分复制流程;
如果回复-ERR,说明redis主节点版本过低,不支持psync命令;
七、哨兵
1、哨兵的作用
Redis的主从复制模式下,一旦主节点挂了,不仅需要人工进行主从切换,还需要通知客户端更换到其他节点上,这种方式效率较低,于是redis就提供了哨兵(Redis Sentinel)来解决这个问题。
2、哨兵原理
每个redis sentinel都是一个单独的进程(一般为多个,且是奇数),每个进程会和节点建立tcp长连接,定时发生心跳包。若规定时间内未得到回应,该哨兵就会认为此节点出现了故障,若此节点是主节点,其他哨兵也会来判断主节点是否是真的挂了,若哨兵达成共识认为主节点挂了,此时哨兵之间会通过raft算法选举出一个哨兵领导角色,由该哨兵负责在从节点中挑选出一个主节点,被挑选出的从节点执行slaveof no one,并且其他从节点要和新选出的主节点建立主从联系。哨兵节点也会自动通知客户端新的主节点,并且后续客户端进行写操作是写入到了新的主节点中。后续如果旧的主节点恢复了,它会成为一个从节点。
3、基于docker搭建redis哨兵环境
(1)基于ubuntu下安装docker
①安装依赖
②添加Docker官方GPG密钥
③添加Docker的软件源
④安装docker
⑤配置用户组
⑥安装工具
⑦重启docker
⑧查看版本
(2)安装docker-compose
(3)停止之前的redis服务器
(4)使用docker获取redis的镜像
(5)docker-compose进行容器编排
此时我们需要6个容器,其中3个Redis服务器(1主2从),3个哨兵节点。
可以使用yml配置文件进行批量启动,3个redis服务器为1个配置文件,3个哨兵节点为1个配置文件。
①创建两个目录,分别放redis和哨兵节点的配置
②配置redis服务器的容器
③创建服务器的容器并启动
④配置redis哨兵节点的容器
⑤创建redis节点容器并启动
此时若直接使用命令redis-compose up -d会报错,是因为哨兵节点需要监控主节点或从节点中的一个,但这两部分是分成两部分来创建的容器,处于不同的局域网中,这两部分是不能互通的,此时需要将哨兵节点加入到主从节点局域网中。
查找主从节点的局域网:
将哨兵节点加入到主从节点的局域网中:
启动哨兵节点:
4、查看哨兵效果
(1)客户端分别连接不同端口服务器并查看服务器角色
(2)模仿主节点服务器宕机 观察其他从节点
其他从节点中的一个被改为主节点。此时,即使原本的主节点恢复正常,它也依旧是主节点,原本的主节点被改为从节点。
八、集群
1、集群概念
(1)广义的集群
指多个机器构成了分布式系统。
(2)狭义的集群
redis提供的集群模式指狭义的集群,主要是拓展节点的存储空间,解决存储空间不足的问题。
2、集群的作用
虽然redis在主从复制中可以拥有多个服务器支持读请求,但是此时每个节点中存储的依旧是全量数据,若数据过多,存储空间会不足,此时就需要多个机器存储数据,每个机器存储部分数据,且每个机器又会搭配一组从节点和哨兵节点。
3、集群原理
每个红框部分可以称为一个分片。
4、数据分片算法
对于增加的数据,应如何进行分片呢???
(1)哈希求余
①原理:假设有N个分片,编号为[0,N-1]。对于每个key,计算hash值,hash值%N,结果即为分片编号。
②优点:简单高效,数据分配均匀。
③缺点:一旦分片进行扩容,就需要对全部数据进行重新分配,需要搬运的数据较大,开销较多。
(2)一致性哈希算法
①原理
先将0-->2^32-1个数据空间映射到圆环上,数据按照顺时针方向增长。
假设现在有三个分片:
对于key求完hash值后,hash值在哪个区间内,即就属于对应分片。
若此时增加一个分片:
此时就需要对0号分片的数据进行重分配,判断是属于0号分片还是3号分片,但1号分片和2号分片对应数据不需要改变。
②优点:大大降低了分片扩容时数据搬运的规模,提高扩容操作效率。
③缺点:分片对应数据分配不均匀。
(3)哈希槽分区算法--redis使用
①原理:
假设有三个分片,将哈希值映射到16384个槽位上(哈希值%16384),将这16384个槽位均匀的分配给每个分片上。
0号分片:[0,5461],共5462个槽位;
1号分片:[5462,10923],共5462个槽位;
2号分片:[10924,16384],共5460个槽位。
此时就可以根据key计算hash值,将hash值进行映射得到哈希槽位,根据哈希槽位找到区间判断是几号分片即可。
若此时增加一个分片,一种可能的分配方式:
0号分片:[0,4095],共4096个槽位;
1号分片:[5462,9557],共4096个槽位;
2号分片:[10924,15019],共4096个槽位。
3号分片:[4096,5461]+[9558,10923]+[15019,16384],共4096个槽位。
ps:此时大家一定有个疑问,为什么是16384个槽位呢???
这主要是基于性能和资源利用的考虑,在redis节点发生心跳包时,需要将槽信息放入心跳包中,以便让节点了解当前集群的状态,若槽的数量过多,会导致心跳包的大小增加,进而增加网络传输负担和节点处理压力;若槽的数量过少,则可能无法充分利用集群的资源。因此,考虑到集群的稳定性和资源的充分利用,redis设计者选择了16384槽位作为默认槽位,且建议分片数量不要超过1000。
②优点
进行分片扩容或缩容较为方便。
③缺点
计算复杂。
5、基于docker搭建redis集群
(1)创建redis节点
创建11个redis节点,其中3个一组为1个分片,一组中有1个主节点2个从节点,剩余2个用于扩容示范。
考虑到创建节点较多,可以使用脚本:
创建一个目录:redis-cluster
在该目录下创建两个文件,第一个文件放docker配置;第二个文件放shell脚本,可以批量化执行,创建出11个节点的配置文件。
向generate.sh文件中写入脚本:
执行shell脚本:
执行成功后,就生成了11个配置文件:
(2)配置redis容器
向docker-compose.yml文件中添加docker配置(其中某一个redis节点的docker配置):
172.30.0.0是静态ip地址中的网络号,要保证该ip是内网且不能和当前主机上其他网络号冲突(ifconfig命令可以查询)。
(3)启动redis容器
(4)构建集群
包括参与集群的端口号和每个分片包含的从节点个数。
集群信息:包括主从关系和槽位分配。
集群配置成功:
集群信息:
6、集群效果
(1)添加key
直接在101分片中添加key1:
此时发现会报错,是因为key1的hash值为9189,属于102分片,无法在101分片添加。
换一种方式添加key1:
添加-c选项后,就可以自动根据槽位自动进行分片转换了。
注意:在集群环境下,无法操作多个key。
(2)集群中主节点挂掉
挂掉redis1主节点:
此时发现redis5就会成为主节点。
当redis1恢复后,它也是以从节点方式进行运行的:
(3)集群环境下节点故障处理原理
每个节点,每秒钟会随机给一些节点发生ping包,通过节点是否在规定时间返回pong包判断节点是否正常。
假设A节点发生ping包给B节点,在规定时间内若A节点未收到B节点返回的pong包,A就会尝试和B重新进行tcp连接,若没有连接成功,A就会认为B挂掉了,并且通过Gossip协议和其他节点进行沟通,确认B是否挂掉,若超过一半的集群个数认为B挂掉了,此时A就会把B标记为FAIL,并告诉其他节点。
当B是从节点时,此时不会进行故障迁移;当B是主节点时,会进行故障迁移。
故障迁移:由B的从节点(假设为C和D触发故障迁移)。
①从节点先判断自己是否有资格竞选主节点,若太久没有和主节点通信,则从节点失去竞选资格;
②具有竞选资格的从节点会休眠一定时间,休眠时间和offset值有关,值越大,排名越靠前,休眠时间越少。
③休眠时间到的从节点会进行拉票操作,且只有主节点具有投票资格,当从节点的票数超过主节点个数一半时,该节点就会晋升成主节点。
④该节点会把晋升为主节点的消息告诉给其他集群节点,更新保存集群结构信息。
(4)整个集群环境宕机的情形
①一个分片的主从节点全部挂掉;
②一个分片主节点挂掉,但是没有从节点;
③短时间内,集群环境中超过一半的主节点全部挂掉。
7、集群扩容
实现效果:当前集群环境为3个分片,打算扩容为4个分片,110主机为主节点,111主机为从节点。
(1)将110主机对应节点加入集群环境中
110是一个master,但此时还没有槽位:
(2)重新分配哈希槽位
调用命令:
移动1/4的哈希值:
选择哪个节点(110对应的ID)来接受这些移动的哈希值:
选择这些slots从哪些节点搬运(填写all表示所有主节点都要进行搬运,done表示自定义):
重新分配后的slots:
注意:在进行扩容时,搬运key时,对应key是无法进行访问的。
(3)将111主机对应节点加入集群环境中
111主机对应节点为110的从节点:
九、redis典型应用--缓存
数据访问速度:cpu寄存器>内存>硬盘>网络。
可以将数据存储在内存中,作为硬盘的缓存。数据库是将数据存储在硬盘中,redis是将数据存储在内存中,可以使用redis作为数据库的缓存,从而提高数据的访问效率。
1、实现过程
客户端发起查询请求,服务器先查询redis,如果redis中存在则直接返回数据,如果redis中不存在,则再去数据库中查询。
我们需要在redis中存放20%的热点数据,就可以使80%的请求不再真正查询数据库了。
2、热点数据
哪些数据属于热点数据呢???
(1)定期生成
每隔一定周期(一天/一周/一月),对于访问的数据频率进行调查,选取前20%的数据作为热点数据,但这部分热点数据不具有实时性。
(2)实时生成
给redis设置容量上限(配置文件中maxmemory参数),针对每次查询,如果在redis中查到了就直接返回,若干redis中不存在,就在数据库中查询,把查到的结果写入redis中。如果redis容量已经达到上限,此时就会触发缓存淘汰策略(配置文件中可以修改)。持续一段时间后,redis的数据就是热点数据了。
3、缓存淘汰策略
(1)volatile-lru:当redis容量达到上限后,在设置了过期时间的key中,根据最后一次使用时间算法进行淘汰,淘汰最久未使用的。
(2)allkeys-lru:当redis容量达到上限后,在所有key中,根据最后一次使用时间算法进行淘汰,淘汰最久未使用的。
(3)volatile-lfu:当redis容量达到上限后,在设置了过期时间的key中,根据最近一段时间访问次数算法进行淘汰,淘汰访问次数最少的。
(4)allkeys-lfu:当redis容量达到上限后,在所有key中,根据最近一段时间访问次数算法进行淘汰,淘汰访问次数最少的。
(5)volatile-random:当redis容量达到上限后,在设置了过期时间的key中,随机淘汰key。
(6)allkeys-random:当redis容量达到上限后,在所有key中,随机淘汰key。
(7)volatile-ttl:当redis容量达到上限后,在设置了过期时间的key中,越早过期的优先被淘汰。
(8)noeviction:默认策略,当redis容量达到上限后,新写入操作会报错。
4、缓存存在的问题
(1)缓存预热
①问题描述
刚开始启动redis或redis宕机时,此时redis上没有数据,全部请求都会发送到数据库,会给数据库带来压力。
②产生原因
redis上没有热点数据。
③解决方法
提前将热点数据准备好写入reids中。
(2)缓存雪崩
①问题描述
短时间内,redis上的大量key失效,导致数据库压力剧增。
②产生原因
redis宕机或者大量key同时达到过期时间。
③解决方法
部署高可用的redis集群或者对key不设置过期时间。
(3)缓存穿透
①问题描述
对于一些key,redis和数据库中都没有,但一直持续访问数据库获取结果,会给数据库带来压力。
②产生原因
业务设计不合理,比如缺失校验环节,导致非法key持续查询;错误操作导致数据库数据被误删;
③解决方法
针对要查询的key,检验key是否合法;使用布隆过滤器先判断key是否存在,再真正进行查询。
(4)缓存击穿
①问题描述
一些热点数据在redis上过期失效了,导致数据库压力骤增。
②产生原因
热点数据过期失效。
③解决方法
对于热点数据不设置过期时间;服务降级,限制同时请求数据库的并发数。
十、redis典型应用--分布式锁
1、分布式锁的作用
在一个分布式系统中,会涉及到多个redis服务器访问同一个公共资源的情况,此时就需要加锁来避免出现线程安全类似的问题。
但此时加锁有个问题,java的synchronized锁是针对于同一个进程的不同线程加锁,而分布式系统中,一个服务器就代表一个进程,此时就需要分布式锁来实现进程间的加锁。
2、分布式锁的原理
使用redis实现分布式锁。
对于多个服务器实现买票来说:A服务器需要实现买票请求,就在redis中使用setnx创建一个指定的key,相当于加锁。此时若B服务器也想实现买票请求,当在redis中操作setnx时,key已经存在,此时就会设置失败,B服务器是阻塞还是放弃自行决定;当A服务器实现买票请求后,进行delete操作删除key,相当于释放锁,此时其他服务器就可以进行加锁实现买票请求了。
3、分布式锁存在的问题
(1)锁未释放
①问题描述
在上述中,A服务器在买票过程中,此时A服务器突然挂掉了,但key没有删掉(未释放锁),其他服务器也无法设置key(加锁)。
②问题解决
给key设置过期时间,若超过规定时间,则key就会失效(锁释放)。但若超过规定时间,加锁下的操作还未完成,此时释放锁就会出现问题。但也不能将过期时间设置太长,会影响其他服务器加锁操作。此时我们就可以考虑设置动态过期时间。
动态过期时间:刚开始设置一个过期时间(比如1s),当key的过期时间快到达1s时,加锁下的操作还未完成,就使得key再增加1s,一直重复操作,直到加锁下的操作完成,就可以不再增加过期时间了。此时即使服务器挂掉了,过期时间不会增加,到达过期时间就会删除key。对于上述key的过期时间有专门的线程进行管理--看门狗。
(2)加锁解锁不是同一个服务器操作
①问题描述
在上述中,A服务器进行加锁后,B服务器又紧接着进行了解锁,此时可能会出现类似线程安全问题。
②问题解决
在加锁时引入校验机制。
每个服务器都有自己的身份编号,A服务器在redis中进行setnx时,value值填入自己的身份编号,此时B服务器若想删除该key,就需要获取key对应的value值,value值和自己身份编号相同才能删除,不相同则不能删除。
(3)删除key(释放锁)不是原子的
①问题描述
服务器在释放锁时需要先查询,再删除,两步操作不是原子的,存在问题。
②问题解决
为了使解锁操作具有原子性,可以使用redis的lua脚本。
lua也是一个编程语言,lua语言实现的一段逻辑,可以被原子的执行。此时就可以使用lua语言实现key删除逻辑,并将代码编写成一个.lua后缀的文件,后续执行key删除时,就可以使redis服务器执行该lua脚本,使得删除key是原子操作的。
lua语言编写的删除功能:
(4)redis挂掉了
①问题描述
在使用redis实现分布式锁时,也可能会出现redis挂掉的情况。
②问题解决
引入更多的redis节点作为备份,加锁时,给redis节点加锁成功的个数超过redis总节点数的一半时,此时才会加锁成功;其他服务器想加锁,就需要遍历全部redis节点判断是否存在key。释放锁时,也需要遍历全部redis节点删除key。(redlock算法)