redis:基于内存,单线程处理所有客户端请求
Redis五种常用数据结构的底层是怎样的?五种数据结构使用场景。 持久化机制?rdb和aof的优缺点?Redis可以用来做什么?怎么做?Redis的执行效率突然变得很慢,是有什么原因导致的? 缓存穿透/雪崩?如何处理?
定义:一款内存高速缓存数据库(全称远程数据服务);使用C语言编写,Redis是一个key-value存储系统。一个键最大能存储512MB
数据类型:五种数据结构使用场景?
string: string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象。比如计数器,粉丝数,访问量,session共享
list:Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。比如简单的消息队列,关注列表、粉丝列表
set:Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。比如好友关系、共同爱好
zset(sorted set):Redis的Set是string类型的有序集合。每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序。比如排行榜
hash:它是由字段和值组成的map。字段和值都是字符串。常用于存储对象数据结构参考:https://blog.csdn.net/m_sherlock/article/details/120978383
(下面的不常用)
位图 Bit arrays (或bitmaps):使用特殊命令,可以像处理一组位那样处理字符串值:可以设置或清除某个位,统计所有被设置为1的位,找到第一个被设置或未设置的位,等等。
超日志(HyperLogLogs):这是一种概率数据结构,用于估计集合的基数。
流(Streams0): 只追加的集合,它由类map提供抽象日志数据类型的元素组成
个性化功能:除了提供的丰富的数据类型,Redis 还提供了像慢查询分析、性能测试、Pipeline、事务、Lua自定义命令、Bitmaps、HyperLogLog、发布/订阅、Geo 等个性化功能
数据库在进行写操作时到底做了哪些事,主要有下面五个过程:
1.客户端向服务端发送写操作(数据在客户端的内存中)。
2.数据库服务端接收到写请求的数据(数据在服务端的内存中)。
3.服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。
4.操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。
5.磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)
故障分析 ,结合上面的5个流程看一下各种级别的故障:
1.当数据库系统故障时,这时候系统内核还是完好的。那么此时只要我们执行完了第3步,那么数据就是安全的,因为后续操作系统会来完成后面几步,保证数据最终会落到磁盘上。
2.当系统断电时,这时候上面5项中提到的所有缓存都会失效,并且数据库和操作系统都会停止工作。所以只有当数据在完成第5步后,才能保证在断电后数据不丢失。
持久化机制?触发机制?rdb和aof的优缺点?
RDB(Redis database,默认机制):在指定的时间间隔内将内存中的数据集快照以二进制的方式写入磁盘
配置:可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置
save 900 1 #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000 #60秒内容如超过10000个key被修改,则发起快照保存
机制及过程:Redis 使用操作系统的多进程 cow(Copy On Write) 机制来实现RDB快照持久化
1.RDB触发持久化时,自动执行bgsave命令,Redis主线程会先检查是否有子线程在执行RDB/AOF持久化任务,如果有的话,直接返回
主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题
2.Redis主线程执行fork操作创建子线程,这个过程中主线程是阻塞的,Redis不能执行来自客户端的任何命令
3.父线程fork完成后,bgsave命令返回”Background saving started”信息并不再阻塞父线程,并可以响应其他命令
4.子线程会根据Redis主线程的内存快照写入临时RDB文件,持久化完成后,会使用临时RDB文件替换掉原来的RDB文件,然后删除原来的RDB文件(所以绝大部分情况下,Redis都只有一个RDB文件)
该过程中主线程的读写不受影响,但Redis的写操作不会同步到主进程的主内存中,而是会写到一个临时的内存区域作为一个副本
5.子线程完成RDB持久化后,会发消息给主进程,通知RDB持久化完成(将上阶段内存副本中的增量写数据同步到主内存)
触发:
手动触发:
save:会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用
bgsave:与save不同的是该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候
自动触发
根据我们的save m n配置规则触发;
从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发bgsave;
执行debug reload时;
执行shutdown时,如果没有开启aof,也会触发
主从同步(slave和master建立同步机制)
优点:
RDB文件小,非常适合定时备份,用于灾难恢复
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。因为存储的是内存中的数据;而AOF文件中存储的是一条条命令,需要重演命令。 AOF瘦身? 100条同样写 -> 一条写
缺点:
RDB无法做到实时持久化,若在两次bgsave间宕机,则会丢失区间(分钟级)的增量数据,不适用于实时性要求较高的场景
RDB的cow机制中,fork子进程属于重量级操作,并且会阻塞redis主进程
AOF(append only file):记录客户端对服务器的每一次写操作命令,并将这些写操作以Redis协议追加保存到以后缀aof文件末尾,在Redis服务器重启时,会加载运行aof文件的命令,以达到恢复数据的目的
配置:AOF默认是关闭的,通过redis.conf配置文件进行开启
## 此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能
## 只有在“yes”下,aof重写/文件同步等特性才会生效
appendonly yes
## 指定aof文件名称
appendfilename appendonly.aof
## 指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec
appendfsync everysec
## 在aof-rewrite期间,appendfsync是否暂缓文件同步,"no"表示“不暂缓”,“yes”表示“暂缓”,默认为“no”
no-appendfsync-on-rewrite no
## aof文件rewrite触发的最小文件尺寸(mb,gb),只有大于此aof文件大于此尺寸是才会触发rewrite,默认“64mb”,建议“512mb”
auto-aof-rewrite-min-size 64mb
## 相对于“上一次”rewrite,本次rewrite触发时aof文件应该增长的百分比
## 每一次rewrite之后,redis都会记录下此时“新aof”文件的大小(例如A)
## aof文件增长到A*(1 + p)之后,触发下一次rewrite,每一次aof记录的添加,都会检测当前aof文件的尺寸。
auto-aof-rewrite-percentage 100
同步策略:
always:每一条AOF记录都立即同步到文件,性能很低,但较为安全。
everysec:每秒同步一次,性能和安全都比较中庸的方式,也是redis推荐的方式。如果遇到物理服务器故障,可能导致最多1秒的AOF记录丢失。
no:Redis永不直接调用文件同步,而是让操作系统来决定何时同步磁盘。性能较好,但很不安全
Rewrite(瘦身):基于Copy On Write,全量遍历内存中数据,然后逐个序列到AOF文件中。因此AOF rewrite能够正确反应当前内存数据的状态
重写过程中,对于新的变更操作将仍然被写入到原AOF文件中,同时这些新的变更操作也会被Redis收集起来(aof_buf文件)。
当内存中的数据被全部写入到新的AOF文件之后,收集的新的变更操作也将被一并追加到新的AOF文件中。
然后将新AOF文件重命名为appendonly.aof,使用新AOF文件替换老文件(替换成功后会删除旧aof文件),此后所有的操作都将被写入新的AOF文件
AOF重写(bgrewriteaof)和RDB快照写入(bgsave)过程类似,二者都消耗磁盘IO。Redis采取了“schedule”策略:无论是“人工干预”还是系统触发,快照和重写需要逐个被执行
触发:
手动:直接调用bgrewriteaof命令
自动:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机
auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB(我们线上是512MB)。
auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的值
*注:自动触发时机:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。这里的“一倍”和“64M” 可以通过配置文件修改
(aof_current_size > auto-aof-rewrite-min-size ) && (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage
优点:AOF只是追加写日志文件,对服务器性能影响较小,速度比RDB要快,消耗的内存较少
缺点:
AOF方式生成的日志文件太大,需要不断AOF重写,进行瘦身,也会频繁引起磁盘IO。
即使经过AOF重写瘦身,由于文件是文本文件,文件体积较大(相比于RDB的二进制文件)。
AOF重演命令式的恢复数据,速度显然比RDB要慢
混合场景持久化(Redis4.0):将RDB文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分AOF日志很小。
相当于:
大量数据使用粗粒度(时间上)的rdb快照方式,性能高,恢复时间快。
增量数据使用细粒度(时间上)的AOF日志方式,尽量保证数据的不丢失
在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升
其他方式持久化:Master使用AOF,Slave使用RDB快照。
master需要首先确保数据完整性,它作为数据备份的第一选择;slave提供只读服务或仅作为备机,它的主要目的就是快速响应客户端read请求或灾切换
启动时加载
RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。
但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;
只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。
Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败
怎么使用list实现一个消息队列?
利用Redislist特性,可以操作(插入或弹出)list的头和尾,生产者在list头存放消息,消费者从list尾消费消息
主从异步复制?Redis通过将主节点数据复制给从节点的一种机制,复制过程是异步进行的。
*注:每个Redis节点启动时都会自动生成一个40位的伪随机数,作为runId(运行ID)
过程:从节点使用psync命令,与主节点进行数据复制同步
1.从节点发送psync [runId offset]命令给主节点
2.主节点先进行runId和offset与自身的Id和offset对比,然后回复相应指令,并执行相应操作
1)主节点向从节点发送命令 FULLRESYNC {runId} {offset}:如果runId不同或offset是-1,则直接进行全量复制
2)主节点向从节点发送命令 CONTINUE命令:如果runId相同,且offset不是-1,选择增量复制
3)主节点向从节点发送命令 ERR:主节点不支持 2.8 的 psync 命令,将使用 sync 执行全量复制
全量复制:全量复制是 Redis 最早支持的复制方式,也是主从第一次建立复制时必须经历的的阶段。过程如下:
发送 psync 命令(spync ? -1) 从节点第一次连接主节点,不知道主节点的runId
主节点根据命令返回 FULLRESYNC {runId} {offset}
从节点记录主节点 ID 和 offset
主节点 bgsave 并保存 RDB 到本地
主节点发送 RBD 文件到从节点
从节点收到 RDB 文件并加载到内存中
主节点在从节点接受数据的期间,将新数据保存到“复制客户端缓冲区”,当从节点加载 RDB 完毕,再发送过去。(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)
从节点清空数据后加载 RDB 文件,如果 RDB 文件很大,这一步操作仍然耗时,如果此时客户端访问,将导致数据不一致,可以使用配置slave-server-stale-data 关闭.
从节点成功加载完 RBD 后,如果开启了 AOF,会立刻做 bgrewriteaof
增量/部分复制:当从节点正在复制主节点时,如果出现网络闪断和其他异常,从节点会让主节点补发丢失的命令数据,主节点只需要将复制缓冲区的数据发送到从节点就能够保证数据的一致性,相比较全量复制,成本小很多。流程如下:
当从节点出现网络中断,超过了 repl-timeout 时间,主节点就会中断复制连接。
主节点会将请求的数据写入到“复制积压缓冲区”,默认 1MB。
当从节点恢复,重新连接上主节点,从节点会将 offset 和主节点 id 发送到主节点。
主节点校验后,如果偏移量的数后的数据在缓冲区中,就发送 cuntinue 响应 —— 表示可以进行部分复制。
主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态
心跳:主从节点在建立复制后,他们之间维护着长连接并彼此发送心跳命令。关键机制如下:
1.主从都有心跳检测机制,各自模拟成对方的客户端进行通信,通过 client list 命令查看复制相关客户端信息,主节点的连接状态为 flags = M,从节点的连接状态是 flags = S。
2.主节点默认每隔 10 秒对从节点发送 ping 命令,可修改配置 repl-ping-slave-period 控制发送频率。
3.从节点在主线程每隔一秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
4.主节点收到 replconf 信息后,判断从节点超时时间,如果超过 repl-timeout 60 秒,则判断节点下线
注意:为了降低主从延迟,一般把 redis 主从节点部署在相同的机房/同城机房,避免网络延迟带来的网络分区造成的心跳中断等情况
异步复制:主节点不但负责数据读写,还负责把写命令同步给从节点。而写命令的发送过程是异步完成,也就是说主节点处理完写命令后立即返回客户度,并不等待从节点复制完成。步骤如下:
主节点接受处理命令。
主节点处理完后返回响应结果 。
对于修改命令,异步发送给从节点,从节点在主线程中执行复制的命令
1.本文主要分析了 Redis 的复制原理;包括复制过程,数据之间的同步,全量复制的流程,部分复制的流程,心跳设计,异步复制流程。
2.其中,可以看出,RDB 数据之间的同步非常耗时。
3.所以,Redis 做了改变。
即当 Redis 主从直接发生了网络中断,不会进行全量复制;
而是将数据放到缓冲区(默认 1MB)里,在通过主从之间各自维护复制 offset 来判断缓存区的数据是否溢出;
如果没有溢出,只需要发送缓冲区数据即可,成本很小;
反之,则要进行全量复制。
4.因此,控制缓冲区大小非常的重要
转自 https://www.cnblogs.com/luao/p/10682830.html
缓存穿透/雪崩?如何处理?
穿透:客户端不断查询一个缓存和数据库一定不存在的数据,并发量大的情况下就会导致数据库压力过大
解决:
1.接口层增加校验,如用户鉴权校验;或对key规则校验,不符合则直接拦截,复合则尝试2操作;
2.对于数据库中不存在的数据, 也对其在缓存中进行空缓存,即设置默认值Null, 为避免占用资源, 一般过期时间会比较短(5分钟)
3.布隆过滤器(bitmap)。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库
击穿:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库
解决:
1.加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存
2.热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存
雪崩:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决:
1.过期时间打散。缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
3.设置热点数据永远不过期
4.加互斥锁
哨兵(sentinel)?监控,消息通知,故障转移,实现高可用。*注:每个Redis节点仍存储全量数据。
主要功能
1.集群监控:负责监控Redis master和slave进程是否正常工作
1)哨兵机制建立了多个哨兵节点(进程),共同监控数据节点的运行状况。
2)同时哨兵节点之间也互相通信,交换对主从节点的监控状况。
3)每隔1秒每个哨兵会向整个集群:Master主服务器+Slave从服务器+其他Sentinel(哨兵)进程,发送一次ping命令做一次心跳检测
2.消息通知:如果某个Redis实例有故障,哨兵负责发送消息作为报警,通知给管理员或者另外的应用程序
3.故障转移:如果发现master挂了后,就会从slave中重新选举一个master
如果一个主节点没有按照预期工作,Sentinel会开始故障转移过程,把一个从节点提升为主节点,并重新配置其他的从节点使用新的主节点,使用Redis服务的应用程序在连接的时候也被通知新的地址
*注:Redis Sentinel是一个分布式系统,
1).当多个Sentinel同意一个master不再可用的时候,就执行故障检测。这明显降低了错误概率。
主管下线:一个哨兵节点判定主节点down掉是主观下线
客观下先:有半数哨兵节点都主观判定主节点down掉,此时多个哨兵节点交换主观判定结果,才会判定主节点客观下线
2).即使并非全部的Sentinel都在工作,Sentinel也可以正常工作,这种特性,让系统非常的健康
*注:基本上哪个哨兵节点最先判断出这个主节点客观下线,就会在各个哨兵节点中发起投票机制Raft算法(选举算法),最终被投为领导者的哨兵节点完成主从自动化切换的过程
4.配置中心:(客户端中不会记录master的地址,而是记录sentinel的地址,客户端只关心sentinel告知的master地址)如果故障转移发生了,通知client客户端新的master地址(Sentinel给客户端的服务发现提供来源:客户端连接到Sentinels来寻找当前主节点的地址。当故障转移发生的时候,Sentinels将报告新的地址)
哨兵leader选举?
1.在这个选举切换主从的过程,整个redis服务是不可用的。
2.当slave发现自己的master挂了
3.将自己记录的currentEpoch加1,并向其他节点请求投票给自己成为master
4.其他节点收到请求,只有master会回应,判断请求的合法性,并投票,可能会有多个slave请求,每个master只能投一票
5.slave收集master的投票
6.当slave收到的投票超过半数后就可以成为master
*注:slave投票,只有master响应(延时响应:确保其他的master也意识到当前的master挂了,否则master可能会拒绝投票)
延时计算公式Delay=500ms+random(0-500)ms+Slave_rank* 100ms(slave_rank为复制数据的等级,等级越小表示复制数据越多也是为了保证能让拥有最新数据的slave最先发起选举)
集群(cluster)?为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,具有复制、高可用和分片特性,能提高系统的并发量。
为了最大化利用内存,可以采用集群,就是分布式存储。
即每台redis存储不同的内容,共有16384个slot,每个redis节点分得一些slot,hash_slot = crc16(key) mod 16384 找到对应slot,键是可用键,如果有{}则取{}内的作为可用键,否则整个键是可用键
*注:集群至少需要3主3从,且每个实例使用不同的配置文件,主从不用配置,集群会自己选
过程?
1)首先redis-trib.rb会以客户端的形式尝试连接所有的节点,并发送PING命令以确定节点能够正常服务;如果有任何节点无法连接,则创建失败。
2)同时发送 INFO 命令获取每个节点的运行ID以及是否开启了集群功能(即cluster_enabled为1
3)准备就绪后集群会向每个节点发送 CLUSTER MEET ip port命令,这个命令用来告诉当前节点指定ip和port上在运行的节点也是集群的一部分,从而使得6个节点最终可以归入一个集群。
4)然后redis-trib.rb会分配主从数据库节点
分配的原则:尽量保证每个主数据库运行在不同的IP地址上,同时每个从数据库和主数据库均不运行在同一IP地址上,以保证系统的容灾能力
注:3主3从,当1个主故障,大家会给对应的从投票,把从立为主,若没有从数据库可以恢复则redis集群就down了
跳转重定位
当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理;
这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据;
客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表
集群leader选举?(待补充)
事务?
1.Redis使用MULTI, EXEC, DISCARD 和 WATCH 命令来实现事务功能。事务可以一次执行多个命令,并带有两个重要的保证
2.事务中的所有命令都被序列化并按顺序执行。Redis执行事务期间,不会被其它客户端发送的命令打断,事务中的所有命令都作为一个隔离操作顺序执行
3.Redis事务是原子操作,所有命令都执行或者都不执行,使用EXEC 命令触发一个事务中所有命令的执行。
所以,如果一个客户端断在调用EXEC 命令前丢失连接,那么所有的命令不会被执行,相反,如果EXEC 被调用,那么所有命令会被执行(但不保证都成功,下面会介绍)
原子性实现:(单线程处理客户端请求)
1.Redis使用 MULTI 命令标记事务开始,它总是返回OK。
2.MULTI执行之后,客户端可以发送多条命令,Redis会把这些命令保存在队列当中,而不是立刻执行这些命令
3.所有的命令会在调用EXEC 命令之后执行
4.如果不调用EXEC,调用 DISCARD 会清空事务队列并退出事务
*注:当使用 append-only file 方式持久化时,Redis使用单个 write(2) 系统调用将事务写到磁盘上。但是,如果Redis服务器崩溃或被系统管理员以某种硬方式杀死,则可能只注册了部分操作。
Redis重启的时候会检测到这种情况,并返回错误退出。使用 redis-check-aof 工具可以删除部分事务,这样Redis可以重新启动。从2.2起,Redis提供了额外的保证,以类似check-and-set (CAS)的乐观锁形式
一旦事务开始执行,虽然都会执行,但不保证都执行成功的原因?
虽然命令都会被执行,但因为事务内的命令可能出现:1.语法错误的时候才失败;2.要执行的key数据类型不匹配,导致一些命令会执行失败
分布式锁?
出现原因:
多进程间,共同操作一个共享资源,如果想要实现分布式锁,必须借助一个外部系统,所有进程都去这个外部系统上申请加锁;而这个外部系统,必须实现互斥功能(统一时间只能有一个进程操作成功,其他进程在此期间等待或返回失败);这个外部系统可以是Mysql、Redis、zookeeper。为了追求性能,通常选择后两者来实现分布式锁。
特性:
互斥性。在任何时刻,保证只有一个客户端持有锁。
不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
保证上锁和解锁都是同一个客户端
实现方式:
使用MySQL,基于唯一索引。
使用ZooKeeper,基于临时有序节点。
使用Redis,基于setnx命令
Redis(单节点)实现分布式锁:
加锁:使用Redis的SETNX命令实现,表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做
释放锁:使用DEL命令删除这个key即可
问题:
1.死锁问题:当客户端1拿到锁后,程序处理异常/进程挂了,没释放锁,会发生死锁
初步解决:1.申请锁,2.使用EXPIRE给这把锁设置一个「租期」。这两个操作不是原子性的(除非每次都创建事务来保证)。
真正解决:Redis2.6.12版本后,扩展了set命令的参数,如:SET key value EX 10 NX
2.过期时间设置导致问题:
现象:
如果设置了过期时间,进程1持有锁后,没有处理完任务,就被Redis过期机制自动释放了;
此时进程2就可以获取锁处理任务;
进程1处理完任务后,还会进行锁释放操作,此时若进程2持有锁正在处理任务,进程1的锁释放操作释放的就是进程2持有的锁,如此,进程3可能也来了...
导致的问题:
1)正持有的锁被动过期?客户端提供守护线程定时检测锁过期时间,如果锁快要过期了,操作共享资源还未完成,则对锁进行续期(重新设置过期时间)(Java中Redisson已经实现了)
2)释放别人的锁?解决:
1.加锁时,设置独有的唯一标识;
2.释放锁时,先获取 GET,再校验后 DEL。这两个操作也是非原子的,需要借助lua脚本(在执行一个 Lua 脚本时,其它请求必须等待,直到lua脚本处理完成))
//lua脚本如下:
if redis.call("GET",KEYS[1])==ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
问题小结:
1.死锁问题:设置过期时间
2.过期时间不好评估,正持有的锁被动过期:守护进程定时监测,自动续期
3.释放别人的锁:加锁时,存入唯一标识;锁校验释放
Redis单节点严谨加锁过程如下:
1.加锁:set $key $unique_value EX $expire_time NX
2.操作共享资源
3.释放锁:Lua脚本,先 GET 获取锁,比较后,符合条件再 DEL 释放锁
因为单节点Redis,如果发生宕机,就会引发一系列问题,所以大多数情况采用Redis集群来实现分布式锁。
Redis(主从集群+哨兵)实现分布式锁
问题:
如果在主节点上执行SET加锁成功;
此时主节点宕机,SET命令还没同步到从节点(异步复制);
从节点被提升为主节点,那么锁在新的主节点上就丢失了!!!??
解决:
RedLock:(JAVA中的Redisson提供了可重入锁、乐观锁、公平锁、读写多、Redlock)
前提:需要部署多个(奇数)master主库实例(他们之间没有任何关系)
加锁过程:
1.客户端先获取当前时间戳T1
2.客户端依次向多个实例发送加锁(SET命令)请求,并且每个请求(毫秒级,要远小于锁的有效时间)会各自设置锁的过期时间T2;如果某一个实例加锁失败(网络延迟、所已被持有等),会继续向下一个实例申请加锁
3.如果客户端从大多数Master实例上加锁成功,则再获取当前时间戳T3,如果T3-T1
Redlock还是又问题,需要考虑实际的应用场景来用,可以参考知乎好文:怎样实现redis分布式锁?
Redis的执行效率突然变得很慢,是有什么原因导致的?参考Redis为什么变慢了?