一、 Redis基础篇
1、什么是Redis?Redis的特点和功能?(高频)
什么是redis?
Remote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
Redis的特点:
1.内存数据库,速度快,也支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
2.Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
3.Redis支持数据的备份,即master-slave模式的数据备份。
4.支持事务
Redis的功能:
多种数据类型。
Redis持久化。
Redis主从复制。
Redis哨兵。
Redis集群。
2、简单介绍一下Redis优点和缺点?
优点:
1、本质上是一个 Key-Value 类型的内存数据库,很像memcached(分布式的高速缓存系统)
2、整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存
3、因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的Key-Value DB(数据库)
4、Redis最大的魅力是支持保存多种数据结构( string , list , set , hash , sorted set(zset) ),此外单个 value 的最大限制是 1GB,不像memcached只能保存 1MB 的数据
5、Redis也可以对存入的 Key-Value 设置 expire 失效时间,因此也可以被当作一个功能加强版的memcached 来用
缺点:
Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
3、系统中为什么要使用缓存?
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。
操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变之后,同步改变缓存中相应的数据即可!
![](https://img-blog.csdnimg.cn/img_convert/2e29278e8c04ad013bc16d19438aea63.png)
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里,而不用经过数据库。
![](https://img-blog.csdnimg.cn/img_convert/65aa27daae65d30966a09632aae222e1.png)
4、常见的缓存同步方案都有哪些?(高频)
设置有效期:给缓存设置有效期,到期后自动删除
同步方案:更改业务代码,加入同步操作缓存逻辑的代码(数据库操作完毕以后,同步操作缓存)
异步方案:
1、使用消息队列进行缓存同步:更改代码加入异步操作缓存的逻辑代码(数据库操作完毕以后,将要同步的数据发送到MQ中,MQ的消费者从MQ中获取数据,然后更新缓存)
2、使用阿里巴巴旗下的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据(二进制格式的文件),然后在通过canal的客户端获取到数据,更新缓存即可。
5、Redis常见数据结构以及使用场景有哪些?(高频)
1、 string
常见命令:set、get、decr、incr、mget等。
基本特点:string数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。
应用场景:常规计数:微博数,粉丝数等。
2、hash
常用命令: hget、hset、hgetall等。
基本特点:hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。
应用场景:存储用户信息,商品信息等。
3、list
常用命令: lpush、rpush、lpop、rpop、lrange等。
基本特点:类似于Java中的list可以存储多个数据,并且数据可以重复,而且数据是有序的。
应用场景:存储微博的关注列表,粉丝列表等。
4、set
常用命令: sadd、spop、smembers、sunion 等
基本特点:类似于Java中的set集合可以存储多个数据,数据不可以重复,使用set集合不可以保证数据的有序性。
应用场景:可以利用Redis的集合计算功能,实现微博系统中的共同粉丝、公告关注的用户列表计算。
5、sorted set (zset)
常用命令: zadd、zrange、zrem、zcard 等。
基本特点:和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
应用场景:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜等。
6、Redis有哪些数据删除策略?(高频)
数据删除策略:Redis中可以对数据设置数据的有效时间,数据的有效时间到了以后,就需要将数据从内存中删除掉。而删除的时候就需要按照指定的规则进行删除,这种删除规则就被称之为数据的删除策略。
Redis中数据的删除策略:
① 定时删除
概述:在设置某个key 的过期时间的同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。
优点:定时删除对内存是最友好的,能够保证内存的key一旦过期就能立即从内存中删除。
缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分CPU时间,对服务器的响应时间和吞吐量造成影响。
② 惰性删除
概述:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
优点:对CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。
③ 定期删除
概述:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键)。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。
另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用,定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。
7、Redis中有哪些数据淘汰策略?(高频)
数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
常见的数据淘汰策略:
noeviction # 不删除任何数据,内存不足直接报错(默认策略)
volatile-lru # 针对设置了过期时间的key,挑选最近最久使用的数据淘汰(举例:key1是在3s之前访问的, key2是在9s之前访问的,删除的就是key2)
volatile-lfu # 针对设置了过期时间的key,挑选最近最少使用数据淘汰 (举例:key1最近5s访问了4次, key2最近5s访问了9次, 删除的就是key1)
volatile-ttl # 挑选将要过期的数据淘汰
volatile-random # 从所有设置了过期时间的key中,任意选择数据淘汰
allkeys-lru # 针对所有key,挑选最近最少使用的数据淘汰
allkeys-lfu # 针对所有key,挑选最近使用次数最少的数据淘汰
allkeys-random # 针对所有key,任意选择数据淘汰,相当于随机
2、缓存淘汰策略常见配置项
maxmemory-policy noeviction # 配置淘汰策略
maxmemory ?mb # 最大可使用内存,即占用物理内存的比例,默认值为0,表示不限制。生产环境中根据需求设定,通常设置在50%以上。
maxmemory-samples count # 设置redis需要检查key的个数
8、Redis中数据库默认是多少个db,即作用?
Redis默认支持16个数据库,可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用select命令更换数据库。
Redis支持多个数据库,并且每个数据库是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。
9、缓存穿透、缓存击穿、缓存雪崩解决方案?(高频)
加入缓存以后的数据查询流程:
![](https://img-blog.csdnimg.cn/img_convert/6ba8db30ff6a4951f524b178908675a2.png)
缓存穿透:
概述:指查询一个一定不存在的数据,如果从存储层查不到数据,则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
解决方案:
1、查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短
2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询
缓存击穿:
概述:对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大量并发的请求可能会瞬间把 DB 压垮。
解决方案:
1、使用互斥锁:当缓存失效时,不立即去load db,先使用,例如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
2、永远不过期:不要对这个key设置过期时间
缓存雪崩:
概述:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
解决方案:
将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
10、什么是布隆过滤器?(高频)
概述:布隆过滤器(Bloom Filter)是1970年由布隆提出的。。布隆过滤器是一种空间效率很高的数据结构,底层是位数组。
![](https://img-blog.csdnimg.cn/img_convert/7d887dacbb82f3f794a98a59dab07c18.webp?x-oss-process=image/format,png)
作用:布隆过滤器可以用于检索一个元素是否在一个集合中,可以大幅降低存储空间需求。
添加元素:比如我们要存储字符串"百度"
![](https://img-blog.csdnimg.cn/img_convert/ddbf457355430c4ffb57dd3db69c14ab.webp?x-oss-process=image/format,png)
首先用三次哈希函数计算不同的哈希值,再用哈希值与布隆过滤器的长度求余,得到几个不同的值,比如为3,5,7,我们把这几个位置设置为1。
判断数据是否存在:通过对每个元素进行多个哈希函数的映射,将其映射到一个位数组中的多个位置上,并将这些位置标记为1。检索时,对待检索元素进行相同的哈希函数映射,若所有映射位置上的值均为1,则认为该元素可能存在于集合中,因为哈希运算可能就会存在重复的情况;若存在任意一个映射位置上的值为0,则认为该元素肯定不存在于集合中。
如下图所示:
![](https://img-blog.csdnimg.cn/img_convert/d6ab88742b8ecab78c321bc11a873c93.webp?x-oss-process=image/format,png)
存储字符串"智能"两个字,还是用上面的算法得到几个不同的值,比如为5,13,17,我们把这几个位置设置为1。
假设添加完 "百度" 和 "智能" 数据以后,布隆过滤器中数据的存储方式如上图所示,那么此时要判断 "人工" 对应的数据在布隆过滤器中是否存在,假设 "人工" 用上面的算法得到的几个不同的值,分别为5,7,13,按照上述的判断规则 "人工" 应该是存在,但是 "人工" 这个数据在布隆过滤器中压根就不存在,这种情况就属于误判。
误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。
删除元素:布隆过滤器不支持数据的删除操作,因为如果支持删除,那么此时就会影响判断不存在的结果。
使用布隆过滤器:在谷歌的guava缓存工具中提供了布隆过滤器的实现,使用方式如下所示:
pom.xml文件
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
测试代码:
// 创建一个BloomFilter对象
// 第一个参数:布隆过滤器判断的元素的类型
// 第二个参数:布隆过滤器存储的元素个数
// 第三个参数:误判率,默认值为0.03
int size = 100_000 ;
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), size, 0.03);
for(int x = 0 ; x < size ; x++) {
bloomFilter.put("add" + x) ;
}
// 在向其中添加100000个数据测试误判率
int count = 0 ; // 记录误判的数据条数
for(int x = size ; x < size * 2 ; x++) {
if(bloomFilter.mightContain("add" + x)) {
count++ ;
System.out.println(count + "误判了");
}
}
// 输出
System.out.println("总的误判条数为:" + count);
Redis中使用布隆过滤器防止缓存穿透流程图如下所示:
![](https://img-blog.csdnimg.cn/img_convert/1ec365f6f3eb54d7be2866b445f2c0b4.png)
应用场景:由于布隆过滤器可以大幅降低存储空间需求,但有一定的误判率,因此常用于场景例如缓存、垃圾邮件过滤等需要快速查找的应用中。
11、Redis数据持久化有哪些方式?各自有什么优缺点?(高频)
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
RDB:
概念:定期更新,定期将Redis中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是Redis的内存快照
优点:数据文件的大小相比于AOF较小,使用RDB进行数据恢复,速度较快
缺点:比较耗时,存在丢失数据的风险
AOF:
概念:将Redis所执行过的所有指令都记录下来,在下次Redis重启时,只需要执行指令就可以了
优点:数据丢失的风险大大降低了
缺点:数据文件的大小相比于RDB较大,使用AOF文件进行数据恢复的时候速度较慢
12、 如何进行持久化?如何配置?
12.1 RDB -- Redis Database Backup file,数据备份文件,数据快照
执行RDB的命令:
save: Redis主进程来执行RDB,会阻塞其他命令,不建议使用
bgsave, 开启子进程执行RDB,避免主进程受到影响,建议使用
基于配置的方式自动持久化 (底层也是bgsave) :
save 3600 1
save 300 100
save 60 10000
rdbcompression yes # 是否对rdb文件进行压缩
dbfilename dump.rdb # 生成的rdb文件名称
dir ./ # 生成rdb文件位置;启动命令所在目录
12.2 AOF -- Append Only File 追加文件
Redis之AOF的相关配置:
appendonly no -- 开启AOF
appendfilename "appendonly.aof" -- AOF产生的文件名称
appendfsync always -- 每次;执行一个写操作;就持久化一次;
appendfsync everysec -- 每秒
appendfsync no -- 不持久化
Redis中AOF存在的问题是:
set aa bb set aa cc --重复命令导致AOF文件中的内容过多
AOF重写:BGREWRITEAOF -- 使用相同的命令代替
AOF自动重写的相关配置有哪些?
auto-aof-rewrite-percentage 100 # 文件大小比上次超过100%
auto-aof-rewrite-min-size 64mb # AOF文件超过64m
13、Redis支持的数据类型有哪些?其中一个String类型最多存储多大数据?
数据类型:string list set hash zset
String类型的最大数据:
A String value can be at max 512 Megabytes in length
14、Redis都存在哪些集群方案?
在Redis中提供的集群方案总共有三种:
1、主从复制
保证高可用性
实现故障转移需要手动实现
无法实现海量数据存储
2、哨兵模式
保证高可用性
可以实现自动化的故障转移
无法实现海量数据存储
3、Redis分片集群
保证高可用性
可以实现自动化的故障转移
可以实现海量数据存储
15、说说Redis哈希槽的概念?
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽[0,16383],每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
16、Redis中的管道有什么用?
管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。
通俗点:pipeline就是把一组命令进行打包,然后一次性通过网络发送到Redis。同时将执行的结果批量的返回回来
![](https://img-blog.csdnimg.cn/img_convert/44a06f8175c0d91221e75b4b4ab01fc5.png)
17、谈谈你对Redis中事务的理解?(高频)
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis中的事务:Redis事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
18、Redis事务相关的命令有哪几个?(高频)
事务相关的命令:
1、multi:用来组装一个事务
2、exec:执行一个事务
3、discard:取消一个事务
4、watch:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行
5、unwatch:取消 watch 命令对所有key的监视
如下所示:
![](https://img-blog.csdnimg.cn/img_convert/f57a64ba209c0dd798bc6dbe778afbc7.png)
19、Redis如何做内存优化?
尽可能使用散列(hash)表,Redis的散列(hash)表会将一个键和一个散列表在数据库中关联起来,用户可以在散列表中为任意多个字段设置值。散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。
![](https://img-blog.csdnimg.cn/img_convert/bfec26233317f04abeb639ff202279f8.jpeg)
20、Redis是单线的,但是为什么还那么快?(高频)
Redis总体快的原因:
1、完全基于内存的
2、采用单线程,避免不必要的上下文切换和竞争条件
3、使用多路I/O复用模型,非阻塞IO,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快
21、Redis集群如何伸缩?
集群伸缩:
分片集群动态添加和删除节点
重新分配插槽范围
22、如何保障缓存和数据库的一致性?
二、 分布式锁篇
23、什么是分布式锁?
概述:在分布式系统中,多个线程访问共享数据就会出现数据安全性的问题。而由于jdk中的锁要求多个线程在同一个jvm中,因此在分布式系统中无法使用jdk中的锁保证数据的安全性,那么此时就需要使用分布式锁。
作用:可以保证在分布式系统中多个线程访问共享数据时数据的安全性
举例:
在电商系统中,用户在进行下单操作的时候需要扣减库存。为了提高下单操作的执行效率,此时需要将库存的数据存储到Redis中。订单服务每一次生成订单之前需要查询一下库存数据,如果存在则生成订单同时扣减库存。在高并发场景下会存在多个订单服务操作Redis,此时就会出现线程安全问题。
![](https://img-blog.csdnimg.cn/img_convert/57fa44bcd03f53dc2fa49ea2c5d8f40a.png)
分布式锁的工作原理:
![](https://img-blog.csdnimg.cn/img_convert/624a5137a04dc1bd6de055eaae21ebec.png)
分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
2、高可用的获取锁与释放锁
3、高性能的获取锁与释放锁
4、具备可重入特性
5、具备锁失效机制,防止死锁
可重入特性:获取到锁的线程再次调用需要锁的方法的时候,不需要再次获取锁对象。
使用场景:遍历树形菜单的时候的递归调用。
注意:锁具备可重入性的主要目的是为了防止死锁。
24、分布式锁的实现方案都有哪些?(高频)
分布式锁的实现方案:
基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁
25、Redis怎么实现分布式锁思路?(高频)
Redis实现分布式锁主要利用Redis的setnx命令。setnx是set if not exists(如果不存在,则 set)的简写。
127.0.0.1:6379> setnx lock value1 #在键lock不存在的情况下,将键key的值设置为value1
(integer) 1
127.0.0.1:6379> setnx lock value2 #试图覆盖lock的值,返回0表示失败
(integer) 0
127.0.0.1:6379> get lock #获取lock的值,验证没有被覆盖
"value1"
127.0.0.1:6379> del lock #删除lock的值,删除成功
(integer) 1
127.0.0.1:6379> setnx lock value2 #再使用setnx命令设置,返回1表示成功
(integer) 1
127.0.0.1:6379> get lock #获取lock的值,验证设置成功
"value2"
上面这几个命令就是最基本的用来完成分布式锁的命令。
加锁:使用setnx key value命令,如果key不存在,设置value(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。
解锁:使用del命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx命令进行加锁。
代码演示:
1.加入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2.加锁和释放锁的工具类:
@Component
public class RedisLock {
// 加锁的代码
public boolean tryLock(Jedis jedis , String key , String requestId) {
Long value = jedis.setnx(key, requestId); // 获取锁
return value == 1 ;
}
// 释放锁
public void releaseLock(Jedis jedis , String key , String requestId) {
String value = jedis.get(key);
if(requestId.equals(value)) { // 同一个客户端的请求才允许进行锁的释放
jedis.del(key) ;
}
}
}
业务代码改造:
@Autowired
private RedisLock redisLock ;
@Override
public void saveOrder(String goodsId) throws InterruptedException {
// 获取锁对象
Jedis jedis = new Jedis("192.168.136.130" , 6379) ;
String requestId = UUID.randomUUID().toString().replace("-" , "") ;
boolean tryLock = redisLock.tryLock(jedis, "lock:" + goodsId, requestId);
if(tryLock) {
try {
// 获取库存数据
String stock = redisTemplate.opsForValue().get("goods_stock:" + goodsId);
if("".equals(stock) || stock == null) {
throw new RuntimeException("库存数据不存在");
}
// 判断是否存在库存,如果不存在直接抛出异常
Integer integerStock = Integer.parseInt(stock) ;
if(integerStock <= 0) {
throw new RuntimeException("库存不足.....");
}else {
// 让线程休眠一会,其他线程获取的到CPU的执行权
TimeUnit.SECONDS.sleep(1);
// 如果存在生成订单,扣减库存
redisTemplate.opsForValue().set("goods_stock:" + goodsId , String.valueOf(--integerStock));
// 生成订单数据
System.out.println("订单生成成功.........");
}
}finally {
// 释放锁
redisLock.releaseLock(jedis , "lock:" + goodsId, requestId);
jedis.close();
}
}else {
throw new RuntimeException("分布式锁获取失败,下单失败");
}
}
3、Jmeter测试
|
![](https://img-blog.csdnimg.cn/img_convert/81f8726c6dd9627bd2387fe8e334d655.png)
| ------------------------------------------------------------ |
整个加锁的逻辑如下图所示:
![](https://img-blog.csdnimg.cn/img_convert/b47f7e0bcdcec76317fecd3e7075adbb.png)
26、Redis实现分布式锁如何防止死锁现象?(高频)
产生死锁的原因:如果一个客户端持有锁的期间突然崩溃了,就会导致无法解锁,最后导致出现死锁的现象。
![](https://img-blog.csdnimg.cn/img_convert/763f7f563bf83b62490a4f04f1207af5.png)
所以要有个超时的机制,在设置key的值时,需要加上有效时间,如果有效时间过期了,就会自动失效,就不会出现死锁。然后加锁的代码就会变成这样。
// 加锁的代码
// requestId描述请求的唯一性,哪一个线程加锁了在解锁的时候就需要使用哪一个线程
public static boolean tryLock(Jedis jedis , String key, String requestId , int expireTime) {
SetParams setParams = new SetParams();
setParams.nx() ;
setParams.ex(expireTime) ;
return "OK".equalsIgnoreCase(jedis.set(key , requestId , setParams)); // 不存则保存成功返回的是OK
}
执行流程如下所示:
![](https://img-blog.csdnimg.cn/img_convert/0fbd2c1cc5d1cac0c9797e44e76a6983.png)
27、Redis实现分布式锁如何合理的控制锁的有效时长?(高频)
有效时间设置多长,假如我的业务操作时间比有效时间长?我的业务代码还没执行完就自动给我解锁了,不就完蛋了吗。
解决方案:
1、第一种:
程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。
2、第二种:给锁续期。
锁续期实现思路:当加锁成功后,同时开启守护线程,默认有效期是用户所设置的,然后每隔10秒就会给锁续期到用户所设置的有效期,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。
![](https://img-blog.csdnimg.cn/img_convert/b570d82ce4d2428f45ec902f99e52f66.png)
上述的第二种解决方案可以使用redis官方所提供的Redisson进行实现。
Redisson是Redis官方推荐的Java版的Redis客户端。它提供的功能非常多,也非常强大分布式服务,使用Redisson可以轻松的实现分布式锁。Redisson中进行锁续期的这种机制被称为"看门狗"机制。redission支持4种连接redis方式,分别为单机、主从、Sentinel、Cluster 集群。
使用步骤如下:
1.加入依赖
<!-- spring boot和redisson整合的时候所对应的起步依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.2</version>
</dependency>
2.定义配置类
@Configuration
public class RedissionConfiguration {
@Bean
public RedissonClient getRedissonClient() {
Config config = new Config() ; // 创建一个配置类
config.useSingleServer().setAddress("redis://192.168.136.130:6379") ; // 使用单节点的服务器,并设置服务器的地址信息
return Redisson.create(config) ; // 创建RedissonClient对象
}
}
3.业务代码加入分布式锁
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Autowired
private RedissonClient redissonClient ;
@Override
public void saveOrder(String goodsId) throws InterruptedException {
// 获取锁对象
RLock lock = redissonClient.getLock("lock:" + goodsId); // 使用hash结构存储锁数据
boolean tryLock = lock.tryLock(3, TimeUnit.SECONDS); // 设置尝试获取锁的最大等待时间
if(tryLock) {
try {
// 获取库存数据
String stock = redisTemplate.opsForValue().get("goods_stock:" + goodsId);
if("".equals(stock) || stock == null) {
throw new RuntimeException("库存数据不存在");
}
// 判断是否存在库存,如果不存在直接抛出异常
Integer integerStock = Integer.parseInt(stock) ;
if(integerStock <= 0) {
throw new RuntimeException("库存不足.....");
}else {
// 让线程休眠一会,其他线程获取的到CPU的执行权
TimeUnit.SECONDS.sleep(1);
// 如果存在生成订单,扣减库存
redisTemplate.opsForValue().set("goods_stock:" + goodsId , String.valueOf(--integerStock));
// 生成订单数据
System.out.println("订单生成成功.........");
}
}finally {
// 释放锁
lock.unlock();
}
}else {
throw new RuntimeException("分布式锁获取失败,下单失败");
}
}
}
4.Jmeter测试
|
![](https://img-blog.csdnimg.cn/img_convert/bdaed55c91ca09615a3867a2acd4314d.png)
|| ------------------------------------------------------------ |
28、Redis实现分布式锁如何保证锁服务的高可用?(高频)
解决方案:
1、使用Redis的哨兵模式构建一个主从架构的Redis集群
2、使用Redis Cluster集群
29、Redis分布式锁的优缺点
方案一:setnx + expier
优点:实现简单,通过修改过期时间可以支持锁重入,锁超时自动释放;
缺点:因为上述命令是分两步执行,如果第二步执行失败,将造成无法解锁。
方案二:setnx + value值是(系统时间+过期时间)
优点:加锁是原子操作,解决了方案一的缺点。
缺点:实现复杂,每个机器的时间必须保持同步,其他加锁线程会修改过期时间,锁有可能被其他线程错误释放。
方案三:使用Lua脚本(包含setnx + expier两条指令)
优点:保证加锁和解锁的原子性。
缺点:无法支持锁的重入,主从模式可能造成锁丢失,锁无法自动续期。
方案四:set的扩展命令(set ex px nx)
优点:保证加锁和解锁的原子性。
缺点:锁有可能被其他线程错误释放,锁没有自动续期机制,锁无法支持重入。
方案五:set ex px nx + 校验唯一随机值,再释放锁
优点:保证加锁的原子性,使用LUA释放锁的话,锁不会被其他线程错误释放。
缺点:锁没有自动续期机制,锁无法支持重入。
方案六: 开源框架:Redisson
优点:锁支持自动续期。
缺点:主从模式可能造成锁丢失。
方案七:多机实现的分布式锁Redlock
优点:锁支持自动续期,解决了方案六的缺点。
缺点:需要部署多台redis机器,极端情况下,会造成两个线程同时获得锁。为了避免该种情况发生,要求宕机的redis在超过锁超时时间后再重启。使用锁的时间要小于锁超时时间。
30、当同步锁数据到从节点之前,主节点宕机了导致锁失效,那么此时其他线程就可以再次获取到锁,这个问题怎么解决?(高频)
使用Redission框架中的RedLock进行处理。
RedLock的方案基于2个前提:
1、不再需要部署从库和哨兵实例,只部署主库
2、但主库要部署多个,官方推荐至少5个实例
也就是说,想使用RedLock,你至少要部署5个Redis实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
![](https://img-blog.csdnimg.cn/img_convert/f3444d6d5bf1a8ac42914a380793ffa5.png)
工作流程如下所示:
1、客户端先获取【当前时间戳T1】
2、客户端依次向这个5个Redis实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时,锁被其他的人持有等各种异常情况),就立即向下一个Redis实例申请加锁
3、如果客户端从 >=3 个(大多数)以上Redis实例加锁成功,则再次获取【当前时间戳T2】, 如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则加锁失败
4、加锁成功,去操作共享资源
5、加锁失败,向【全部节点】发起释放锁请求
总结4个重点:
1、客户端在多个Redis实例上申请加锁
2、必须保证大多数节点加锁成功
3、大多数节点加锁的总耗时,要小于锁设置的过期时间
4、锁释放,要向全部节点发起释放锁请求
30.1 为什么要在多个实例上加锁?
本质上是为了【容错】, 部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
30.2 为什么步骤3加锁成功后,还要计算加锁的累计耗时?
因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。所以,即使大多数节点加锁成功,如果加锁的累计耗时已经超过了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
代码大致如下所示:
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:5378").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.1:5379").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.1:5380").setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
String resourceName = "REDLOCK_KEY";
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
// isLock = redLock.tryLock();
// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
System.out.println("isLock = "+isLock);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
redLock.unlock();
}