1. 什么是Redis?它主要用来什么的?
Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
2.说说Redis的基本数据结构类型
大多数小伙伴都知道,Redis有以下这五种基本类型: String(字符串) Hash(哈希) List(列表) Set(集合) zset(有序集合) ==它还有三种特殊的数据结构类型== Geospatial Hyperloglog Bitmap
2.1 Redis 的五种基本数据类型
String(字符串) : 简介:String是Redis最基础的数据结构类型,它是==二进制安全的,可以存储图片或者序列化的对象,值最大存储为512M== : 简单使用举例: set key value、get key等 : 应用场景:共享session、分布式锁,计数器、限流。 : 内部编码有3种,int(8字节长整型)/embstr(小于等于39字节字符串)/raw(大于39个字节字符串)
C语言的字符串是char[]实现的,而Redis使用SDS(simple dynamic string) 封装,sds源码如下:
struct sdshdr{
unsigned int len; // 标记buf的长度
unsigned int free; //标记buf中未使用的元素个数
char buf[]; // 存放元素的坑
}
Redis为什么选择SDS结构,而C语言原生的 char[]不香吗? 举例其中一点,SDS中,O(1)时间复杂度,就可以获取字符串长度;而C 字符串,需要遍历整个字符串,时间复杂度为O(n)
Hash(哈希) : 简介:在Redis中,哈希类型是指v(值)本身又是一个==键值对(k-v)结构== 简单使用举例:hset key field value 、hget key field 内部编码:ziplist(压缩列表) 、hashtable(哈希表) 应用场景:缓存用户信息等。 注意点:如果开发使用hgetall,哈希元素比较多的话,可能导致Redis阻塞,可以使用hscan。而如果只是获取部分field,建议使用hmget。
字符串和哈希类型对比如下图:
List(列表) : 简介:列表 [list] 类型是用来存储多个==有序==的字符串,一个列表最多可以存储2^32-1个元素 简单实用举例: lpush key value [value ...] 、lrange key start end 内部编码:ziplist(压缩列表)、linkedlist(链表) 应用场景: 消息队列,文章列表,
一图看懂list类型的插入与弹出: list应用场景参考以下:
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
Set(集合) : 简介:集合(set)类型也是用来保存多个的字符串元素,==但是不允许重复元素== 简单使用举例:sadd key element [element ...]、smembers key 内部编码:intset(整数集合)、hashtable(哈希表) 注意点:smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,可以***can来完成。 应用场景: 用户标签,生成随机数抽奖、社交需求。
有序集合(zset) : 简介:已排序的字符串集合,同时元素==不能重复== 简单格式举例:zadd key score member [score member ...],zrank key member 底层内部编码:ziplist(压缩列表)、skiplist(跳跃表) 应用场景:排行榜,社交需求(如用户点赞)。
2.2 Redis 的三种特殊数据类型
: Geo:Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。 HyperLogLog:用来做基数统计算法的数据结构,如统计网站的UV。 Bitmaps :用一个比特位来映射某个元素的状态,在Redis中,它的底层是基于字符串类型实现的,可以把bitmaps成作一个以比特位为单位的数组
3. Redis为什么这么快?
3.1 ==基于内存存储实现== 我们都知道==内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库==,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
3.2 ==高效的数据结构== 我们知道,Mysql索引为了提高效率,选择了==B+树==的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。先看下Redis的数据结构&内部编码图: 1、简单动态字符串 这个名词可能你不熟悉,换成SDS肯定就知道了。这是用来处理字符串的。了解 C 语言的都知道,它是有处理字符串方法的。而 Redis 就是 C 语言实现的,那为什么还要重复造轮子?我们从以下几点来看: ==(1)字符串长度处理== 这个图是字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 '\0' 为止。 Redis 中怎么操作呢?用一个 len 字段记录当前字符串的长度。想要获取长度只需要获取 len 字段即可。你看,差距不言自明。前者遍历的时间复杂度为 O(n),Redis 中 O(1)就能拿到,速度明显提升。 (2)内存重新分配
C 语言中涉及到修改字符串的时候会重新分配内存。修改地越频繁,内存分配也就越频繁。而内存分配是会消耗性能的,那么性能下降在所难免。 而 Redis 中会涉及到字符串频繁的修改操作,这种内存分配方式显然就不适合了。于是SDS实现了两种优化策略: ===空间预分配=== ==对 SDS 修改及空间扩充时,除了分配所必须的空间外,还会额外分配未使用的空间。 具体分配规则是这样的:SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。== 惰性空间释放 当然,有空间分配对应的就有空间释放。 SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。
(3)二进制安全 你已经知道了 Redis 可以存储各种数据类型,那么二进制数据肯定也不例外。但二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。 前面我们提到过,C 中字符串遇到'\0'会结束,那'\0'之后的数据就读取不上了。==但在 SDS 中,是根据 len 长度来判断字符串结束的。== 看,二进制安全的问题就解决了。
2、双端链表 列表 List 更多是被当作队列或栈来使用的。队列和栈的特性一个先进先出,一个先进后出。双端链表很好的支持了这些特性。 图注:双端链表 (1)前后节点 链表里每个节点都带有两个指针,prev 指向前节点,next 指向后节点。这样在时间复杂度为O(1)内就能获取到前后节点。 (2)头尾节点 你可能注意到了,头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点。这样的设计能够对双端节点的处理时间复杂度降至O(1),对于队列和栈来说再适合不过。同时链表迭代时从两端都可以进行。 (3)链表长度 头节点里同时还有一个参数 len,和上边提到的 SDS 里类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到 len 值就可以了,这个时间复杂度是O(1)。 你看,这些特性都降低了 List 使用时的时间开销。 3、压缩列表 双端链表我们已经熟悉了。不知道你有没有注意到一个问题:如果在一个链表节点中存储一个小数据,比如一个字节。那么对应的就要保存头节点,前后指针等额外的数据。 这样就浪费了空间,同时由于反复申请与释放也容易导致内存碎片化。这样内存的使用效率就太低了。 于是,压缩列表上场了! 它是经过特殊编码,专门为了提升内存使用效率设计的。所有的操作都是通过指针与解码出来的偏移量进行的。 并且压缩列表的内存是连续分配的,遍历的速度很快。
4、字典 ==Redis 作为 K-V 型数据库,所有的键值都是用字典来存储的。== 日常学习中使用的字典你应该不会陌生,想查找某个词通过某个字就可以直接定位到,速度非常快。这里所说的字典原理上是一样的,通过某个 key 可以直接获取到对应的value。 ==字典又称为哈希表==,这点没什么可说的。哈希表的特性大家都很清楚,能够在O(1)时间复杂度内取出和插入关联的值。
5、跳跃表 作为 Redis 中特有的数据结构-跳跃表,其在链表的基础上增加了多级索引来提升查找效率。 这是跳跃表的简单原理图,每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在O(logN)的时间复杂度里查找到对应的节点。 下面这张是跳表真实的存储结构,和其它数据结构一样,都在头节点里记录了相应的信息,减少了一些不必要的系统开销。 合理的数据编码 对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。
那我们就来看看,不同的数据类型是如何进行编码转化的: : String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。 List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码 Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。 Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。 Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码
合适的线程模型 Redis 快的原因还有一个是因为使用了合适的线程模型: 1、I/O多路复用模型
I/O:网络 I/O 多路:多个 TCP 连接 复用:共用一个线程或进程 生产环境中的使用,通常是多个客户端连接 Redis,然后各自发送命令至 Redis 服务器,最后服务端处理这些请求返回结果。 应对大量的请求,Redis 中使用I/O 多路复用程序同时监听多个套接字,并将这些事件推送到一个队列里,然后逐个被执行。最终将结果返回给客户端。
2、避免上下文切换
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。Redis是面向快速执行场景的数据库。,所以要慎用如smembers和lrange、hgetall等命令。 Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程。 虚拟内存机制 Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
Redis的虚拟内存机制是啥呢?
虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。==通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。==
总结
: 基于内存实现 数据都存储在内存里,减少了一些不必要的 I/O 操作,操作速率很快。 高效的数据结构 底层多种数据结构支持不同的数据类型,支持 Redis 存储不同的数据; 不同数据结构的设计,使得数据存储时间复杂度降到最低。 合理的数据编码 根据字符串的长度及元素的个数适配不同的编码格式。 合适的线程模型 I/O 多路复用模型同时监听客户端连接; 单线程在执行过程中不需要进行上下文切换,减少了耗时。
4. 什么是缓存击穿、缓存穿透、缓存雪崩?
4.1 缓存穿透问题 先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。
==缓存穿透==:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。 ==通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。==
缓存穿透一般都是这几种情况产生的:
- ==业务不合理的设计==,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
- ==业务/运维/开发失误的操作==,比如缓存和数据库的数据都被误删除了。
- ==黑客非法请求攻击==,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。
如何避免缓存穿透呢? 一般有三种方法。
- 1.如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
- 2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
- 3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
4.2 缓存雪崩问题
==缓存雪崩==: 指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。
- 缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒酱紫。
- Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。
4.3 缓存击穿问题
==缓存击穿==: 指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。 缓存击穿看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪崩的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点key缓存,雪崩则是很多key。
解决方案就有两种:
- 使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
- “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
5. 什么是热Key问题,如何解决热key问题
什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。 如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
而热点Key是怎么产生的呢?主要原因有两个:
- 用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
- 请求分片集中,超过单Redi服务器的性能,比如固定名称key,Hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点Key问题。
那么在日常开发中,如何识别到热点key呢?
- 凭经验判断哪些是热Key;
- 客户端统计上报;
- 服务代理层上报;
如何解决热key问题?
- Redis集群扩容:增加分片副本,均衡读流量;
- 将热key分散到不同的服务器中;
- 使用二级缓存,即JVM本地缓存,减少Redis的读请求。
6. Redis 过期策略和内存淘汰策略
Redis的内存过期策略通常有以下三种:
- ==定时过期==:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。) Redis中同时使用了惰性过期和定期过期两种过期策略。
Redis的内存淘汰策略:
==noeviction==:当内存不足以容纳新写入数据时,新写入操作会报错。 ==allkeys-lru==:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。 ==allkeys-random==:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。 ==volatile-lru==:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。 ==volatile-random==:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。 ==volatile-ttl==:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
7.说说Redis的常用应用场景
- 缓存
- 排行榜
- 计数器应用
- 共享Session
- 分布式锁
- 社交网络
- 消息队列
- 位操作
8. Redis 的持久化机制有哪些?优缺点说说
Redis是基于内存的非关系型K-V数据库,既然它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。
Redis提供了RDB和AOF两种持久化机制,它持久化文件加载流程如下:
RDB,就是把内存数据以快照的形式保存到磁盘上。
什么是快照?可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来。
RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。RDB触发机制主要有以下几种: ==RDB 的优点==
- 适合大规模的数据恢复场景,如备份,全量复制等
==RDB的缺点==
- 没办法做到实时持久化/秒级持久化。
- 新老版本存在RDB格式兼容问题
AOF
AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。 AOF的工作流程如下: AOF的优点
- 数据的一致性和完整性更高
AOF的缺点
- AOF记录的内容越多,文件越大,数据恢复变慢。
9.怎么实现Redis的高可用?
我们在项目中使用Redis,肯定不会是单点部署Redis服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。 Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。 9.1 主从模式 主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制 主从复制包括全量复制,增量复制两种。一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制,全量复制流程如下:
- 1.slave发送sync命令到master。
- 2.master接收到SYNC命令后,执行bgsave命令,生成RDB全量文件。
- 3.master使用缓冲区,记录RDB快照生成期间的所有写命令。
- 4.master执行完bgsave后,向所有slave发送RDB快照文件。
- 5.slave收到RDB快照文件后,载入、解析收到的快照。
- 6.master使用缓冲区,记录RDB同步期间生成的所有写的命令。
- 7.master快照发送完毕后,开始向slave发送缓冲区中的写命令;
- 8.salve接受命令请求,并执行来自master缓冲区的写命令
redis2.8版本之后,已经使用psync来替代sync,因为sync命令非常消耗系统资源,psync的效率更高。 slave与master全量同步之后,master上的数据,如果再次发生更新,就会触发增量复制。 当master节点发生数据增减时,就会触发replicationFeedSalves()函数,接下来在 Master节点上调用的每一个命令会使用replicationFeedSlaves()来同步到Slave节点。执行此函数之前呢,master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且slave节点不为空,就会执行此函数。这个函数作用就是:把用户执行的命令发送到所有的slave节点,让slave节点执行。流程如下: 9.2 哨兵模式 主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis从2.8开始正式提供了Redis Sentinel(哨兵)架构来解决这个问题。 哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控。
简单来说,哨兵模式就三个作用:
- 发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
- 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
- 哨兵之间还会相互监控,从而达到高可用。
故障切换的过程是怎样的呢
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
哨兵的工作模式如下:
- 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个 PING命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel标记为主观下线。
- 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态 4. 当有足够数量的Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态,则Master会被标记为客观下线。 5. 在一般情况下, 每个 Sentinel会以每10秒一次的频率向它已知的所有Master,Slave发送 INFO 命令。 6. 当Master被 Sentinel标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 7. 若没有足够数量的 Sentinel同意Master已经下线, Master的客观下线状态就会被移除;若Master 重新向Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
9.3 Cluster集群模式 哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。 因此,Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。
10. 使用过Redis分布式锁嘛?有哪些注意点呢?
分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis作为分布式锁。
选了Redis分布式锁的几种实现方法,大家来讨论下,看有没有啥问题哈。
- 命令setnx + expire分开写
- setnx + value值是过期时间
- set的扩展命令(set ex px nx)
- set ex px nx + 校验唯一随机值,再删除
10.1 命令setnx + expire分开写
if(jedis.setnx(key,lock_value) == 1){ //加锁
expire(key,100); //设置过期时间
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就“长生不老”了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。
10.2 setnx + value值是过期时间
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
笔者看过有开发小伙伴是这么实现分布式锁的,但是这种方案也有这些缺点:
过期时间是客户端自己生成的,分布式环境下,每个客户端的时间必须同步。 没有保存持有者的唯一标识,可能被别的客户端释放/解锁。 锁过期的时候,并发多个客户端同时请求过来,都执行了jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
10.3: set的扩展命令(set ex px nx)(注意可能存在的问题)
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
这个方案可能存在这样的问题:
- 锁过期释放了,业务还没执行完。
- 锁被别的线程误删。'
10.4 set ex px nx + 校验唯一随机值,再删除
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key))) {
jedis.del(key); //释放锁
}
}
}