01 Redis到底是单线程还是多线程
- Redis 6.0 版本之前的单线程指的是其网络I/O和键值对读写是由一个线程完成的
- Redis 6.0引入的多线程指的是网络请求过程采用了多线程,而键值对读写命令仍然是单线程处理的,所以Redis依然是并发安全的。
- 只有网络请求模块和数据操作模块是单线程,而其他的持久化,集群数据同步等,其实是由额外的线程执行的。
- 总结:6.0网络I/O是多线程,数据操作是单线程。
02 Redis单线程为什么还能很快
- 命令执行基于内存操作,一条命令在内存里操作的时间是几十纳秒
- 命令执行是单线程操作,没有线程切换开销
- 基于IO多路复用机制提升Redis的I/O利用率(Linux使用epoll)
-
高效率的数据存储结构:全局hash表以及多种高效数据结构,比如:跳表,压缩列表,链表等等
-
03 Redis底层数据是如何用跳表来存储的
跳表:将有序链表改造为支持近似“折半查找”算法,可以进行快速的插入,删除,查找操作
04 Redis Key过期了为什么内存没有释放
SET除了可以设置key-value之外,还可以设置key的过期时间。
如果想要修改key的值,使用set命令,而没有加上过期时间的参数,那么这个key的过期时间将会被擦除。
导致这个问题的原因在于:SET命令如果不设置过期时间,那么Redis会自动擦除这个key的过期时间,如果你发现Redis的内存持续增长,而且很多key原来设置了过期时间,后来发现过期时间丢失了,很多都是这个原因导致的。
这时你的Redis中就会存在大量不过期的key,消耗过多的内存资源,所以,你在使用SET命令时,如果刚开始设置了过期时间,那么之后修改这个key,也务必要加上过期时间的参数,避免过期时间丢失的问题。
Redis对于过期key的处理一般有惰性删除和定时删除两种策略
- 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,判断key是否过期,如果过期了直接删除掉这个key.
- 定时删除:由于惰性删除策略无法保证冷数据被即使删除掉,所以Redis会定期(默认每100ms)主动淘汰一批过期的key,这里的一批只是部分过期key,所以可能会出现部分key已经过期但还没有被清理掉的情况,导致内存并没有被释放。
05 Redis Key没设置过期时间为什么被Redis主动删除了
当Redis已用内存超过maxmemory限定时,触发主动清理策略。
主动清理策略在Redis4.0之前一共实现了6种内存淘汰策略,在4.0之后,又增加了2种策略,总共8种:
- a)正对设置了过期时间的key做处理:
- volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期越先被删除。
- volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
- volatile-lru:会使用LRU算法筛选设置了过期时间的键值对删除。
- volatile-lfu:会使用LFU算法筛选设置了过期时间的键值对删除。
- b)针对所有的key做处理
- allkeys-random:从所有键值对中随机选择并删除数据。
- allkeys-lru:使用LRU算法在所有数据中进行筛选删除。
7.allkeys-lfu:使用LFU算法在所有数据中进行筛选删除。
- c)不做处理
- noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error)OOM command not allowed when used memory",此时Redis响应读操作。
06 Redis淘汰Key的算法LRU与LFU区别
LRU算法(Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一次访问时间作为参考。
LFU算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的数据,以此作为参考,绝大多数情况下我们都可以用LRU策略,当存在大量的热点缓存数据时,LFU可能过更好点。
07 删除Key的命令会阻塞Redis吗?
DEL
格式:DEL key [key ...]
删除给定的一个或多个key.
不存在的key会被忽略。
可用版本:
>= 1.0.0
时间复杂度:
O(N),N为被删除的key的数量
删除单个字符串类型的key,时间复杂度为0(1)
删除单个列表,集合,有序集合或哈希类型的key,时间复杂度为O(M),M为上数据结构内的元素数量。
返回值:
被删除key的数量。
08 Redis主从,哨兵,集群架构优缺点比较
-
主从模式
-
哨兵模式
在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微负载,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置的过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率。
-
高可用集群模式
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制,高可用和分片特性。Redis集群不需要sentinel哨兵也能完成系节点移除和故障转移的功能,需要将每个节点设置成集群模式,这种集群模式没有中心节点,可以水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。Redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
09 Redis集群数据hash分片算法是怎么回事
Redis Cluster将所有数据划分为16384个slots(槽位),每个节点负责其中一部分的槽位。槽位的信息存储于每个节点中。
当Redis Cluster的客户端来连接结群时,他也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个key时,可以根据槽位定位算法定位到目标节点。
槽位定位算法
Cluster默认会对key值使用crc16算法进行hash得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位。
HASH_SLOT = CRC16(key) mod 16384
再根据槽位值和Redis节点的对应关系就可以定位到key具体落在那个Redis节点上的。
10 Redis执行命令竟然有死循环阻塞Bug
RANDOMKEY
格式:randomkey
从当前数据库中随机返回(不删除)一个key。
可用版本:
>= 1.0.0
时间复杂度:
O(1)
返回值:
当数据库不为空时,返回一个key.
当数据库不为空时,返回null。
示例代码:
如果你想随机查看Redis中的一个key, Redis里有一个RANDOMKEY命令可以从Redis中随机取出一个key,这个命令可能导致Redis死循环阻塞。
前面的面试题讲过Redis对于过期Key的清理策略是定时删除与惰性删除两种方式结合来做的,而RANDOMKEY在随机拿出一个key后,首先会先检查这个key是否已经过期,如果该key已经过期,那么Redis会删除它,这个过程就是惰性删除。但清理完了还不能结束,Redis还要找出一个没过期的key,返回给客户端。
此时,Redis则会继续随机拿出一个key,然后再判断它是否过期,直到找出一个过期的key返回给客户端。
这里就有一个问题了,如果此时Redis中,有大量key已经过期,但还未来得及被清理掉,那这个循环就会持续很久才能结束,而且,这个耗时都花费在了清理过期key以及寻找不过期key上,导致结果就是,RANDOMKEY执行耗时变长,影响Redis性能。
以上流程,其实是在maser上执行的。
如果在slave上执行RANDOMKEY,那么问题会更严重。
slave自己是不会清理过期key,当一个key要过期时;master会先清理删除它,之后master向slave发送一个DEL命令,告知slave也删除这个key,那再slave上执行RANDOMKEY时,就会发生以下问题:
1.slave随机取出一个key,判断是否已过期。
2.key已过期,但slave不会删除它,而是继续随机寻找不过期的key.
3.由于大量key都已过期,那slave就会寻找不到符合条件的key,此时就会陷入死循环。
也就是说,在slave上执行RANDOMKEY,有可能会造成整个Redis实例卡死。
这其实就是Redis的一个Bug,这个Bug一直持续到5.0才被修复,修复的解决方案就是在slave中最多找一定的次数,无论是否能找到,都会推出循环。
11 一次线上事故,Redis主从切换导致了缓存雪崩
我们假设,slave的机器时钟比master走得快很多。
此时,Redis master里设置了过期时间的key,从slave角度来看,可能会有很多在master里没过期的数据其实已经过期了。
如果此时操作主从切换,把slave提升为新的master.
它成为master后,就会开始大量清理过期key,此时就会导致一下结果:
1.master大量清理过期key,主线程可能会发生阻塞,无法即使处理客户端请求。
2.Redis中数据大量过期,引发缓存雪崩。
当master与slave机器时钟严重不一致时,对业务的影响非常大。
所以,我们一定要保证主从库的机器时钟一致性,避免发生这些问题。
12 Redis持久化RDB,AOF,混合持久化是怎么回事
RDB快照(snapshot)
在默认情况下,Redis将内存数据库快照保存在名字为dump.rdb的二进制文件中。
你可以对Redis进行设置,让它在"N秒内数据集至少有M个改动“这一条件被满足时,自动保存一次数据集。比如说,以下设置会让Redis在满足”60秒内有至少有1000个键被该栋“这一条件时,自动保存一次数据集。
# save 60 1000 #关闭RDB只需要将所有的save保存策略注释掉即可
还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件,每次命令执行都会将所有redis内存快照到一个新的rdb文件中里,并覆盖原有rdb快照文件。
bgsave的写时复制(COW)机制
Redis借助操作系统提供的写时复制技术(Copy-On-Write COW),在生成快照的同时,依然可以正常处理写命令。简单来说,bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件,此时如果主线程对这些数据也都是读操作,那么,主线程和bgsave子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本,然后,bgsave子进程会把这个副本数据写入RDB文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
save与bgsave对比:
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
是否阻塞redis其它命令 | 是 | 否(在生成子进程执行调用fork函数时会有短暂阻塞) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fork子进程,消耗内存 |
配置自动生成rdb文件后台使用的是bgsave方式。
AOF(append-only file)
快照功能并不是非常耐久(durable):如果Redis因为某些原因造成故障停机,那么服务器将都是最近写入,且仍未保存到快照中的那些数据,从1.1版本开始,Redis增加了一种完全耐久的持久化方式:AOF持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)你可以通过修改配置文件来打开AOF功能:
# appendonly yes
从现在开始,每当Redis执行一个改变数据集的命令时(比如SET),这个命令就会追加到AOF文件的末尾。这样的话,当Redis重新启动时,程序就可以通过重新执行AOF文件的命令来达到重建数据集的目的。你可以配置Redis多久才将数据fsync到磁盘一次。
有三个选项:
appendfsync always: 每次有新命令追加到AOF文件时就执行一次fsync,非常慢,非常安全。
appendsync everysec: 每秒fsync一次,足够快,并且在故障时只会都是1秒中的数据。
appendfsync no: 从不fsync,将数据交给操作系统来处理,更快,也更不安全的选择。
推荐(并且也是默认)的措施未每秒fsync一次,这种fsync策略可以兼顾速度和安全性。
AOF重写
AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成aof文件。
执行bgrewriteaof过后
将多条命令转化成一条命令执行。
如果两个配置可以控制AOF自动重写频率
# auto-aof-rewrite-min-size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
# auto-aof-rewrite-percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写
当然AOF还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF。
注意,AOF重写redis会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多影响。
RDB和AOF,我应该用哪一个?
命令 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 容易丢数据 | 根据策略决定 |
生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更安全一点。
Redis 4.0 混合持久化
重启Redis时,我们很少使用RDB来恢复内存状态,因为会丢失大量数据,通常使用AOF日志重放,但是重放AOF日志性能相对RDB来说要慢很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。Redis4.0为了解决这个问题,带来了一个新的持久化选项--混合持久化。
通过如下配置可以开启混合持久化(必须先开启aof):
# aof-use-rdb-preamble yes
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成后旧两个AOF文件的替换。于是在Redis重启的时候,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率大幅度得到提升。
混合持久化AOF文件结构如下:
13 线上Redis持久化策略一般如何设置
如果对性能要求比较高,在Master最好不要做持久化,可以在某个Slave开启AOF备份数据,策略设置为秒同步一次即可。
14 一次线上事故,Redis主节点宕机导致数据全部丢失
如果你的Redis采用如下模式部署,就会发生数据丢失的问题:
- master-slave + 哨兵部署实例。
- master没有开启数据持久化功能
- Redis进程使用supervisor管理,并配置为进程宕机,自动重启。
如果此时master宕机,就会导致下面问题:
- master宕机,哨兵还未发起切换,此时master进程立即被supervisor自动拉起。
- 但master没有开启任何数据持久化,启动后是一个空实例。
- 此时slave为了与master保持一致,它会自动清空实例中所有数据,slave也变成了一个空实例。
在这个场景下,master/slave的数据就全部丢失了。
这时,业务应用在访问Redis时,发现缓存中没有任何数据,就会把请求全部打到后端数据库上,这还会进一步引发缓存雪崩,对业务影响非常大。
这种情况下我们一般不应该给Redis主节点配置进程宕机马上自动重启策略,而应该等哨兵把某个Redis从节点切换为主节点后再重启之前宕机的Redis主节点让其变为slave节点。
15 Redis线上数据如何备份
- 写crontab定时调度脚本,每小时都copy一份rdb或aof文件到另外一台机器中去,保留最近48小时的备份
- 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份。
- 每次copy备份的时候,都把太旧的备份给删除了。
16 Redis主从复制风暴是怎么回事
如果Redis主节点有很多从节点,在某一时刻如果所有从节点都同时连接主节点,那么主节点会同时把内存快照RDB发给多个从节点,这样会导致Redis主节点压力非常大,这就是所谓的Redis主从复制风暴问题。这种问题我们Redis主从架构做一些优化得以避免,比如可以做下面这种树形复制结构。
17 Redis集群网络抖动导致频繁主从切换怎么处理
真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种的小问题,比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。
为了解决这种问题,Redis Cluster提供了一种选项cluster-node-timeout,表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换,如果没有这个选项,网络抖动会导致主从频繁切换(数据的重新复制).
18 Redis集群为什么至少需要三个master节点
因为新master的选举需要大于一半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
19 Redis集群为什么推荐奇数个节点
因为新master的选举需要大于半数的集群master节点同意才能选举成功,奇数个master节点可以满足选举条件的基础上省一个节点,比如三个节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。
20 Redis集群支持批量操作命令吗?
对于类似mset,mget这样的多个key的原生批量操作命令,redis集群中只支持所有key落在同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前面加上{xxx},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key能落到同一solt里去,示例如下:
mset {user1}:1:name zhuge {user1}:1:age 18
假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括号里的user1做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot.
21 Lua脚本能在Redis集群执行吗
Redis官方规定Lua脚本如果想在Redis集群里执行,需要Lua脚本里操作的所有Redis Key落在集群的同一个节点上,这种的话我们可以给Lua脚本的Key前面加一个相同的hash tag,就是{XXX},这样就能保证Lua脚本里所有key落在相同的节点上。
22 什么是Redis
Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源,包含多种数据结构,支持网络,基于内存,可选持久性的键值对存储数据库,具备如下特性:
- 基于内存运行,性能高效(每秒可以处理超过10万次读写操作)
- 支持分布式,理论上可以无限扩展
- key-value存储系统(key是字符串,键有字符串,列表,集合,散列表,有序集合等)。
- 开源的使用ANSI C语言编写,遵守BSD协议,支持网络,可基于内存亦可持久化的日志型,key-Value数据库,并提供多种语言的API。
23 Redis有哪些数据类型?各自的使用场景?
Redis主要有5种数据类型,包括String, List, Set, Zset, Hash, 满足大部分的使用要求。但Redis还为我们提供了几种高级数据结构,bitmaps, HyperLogLong,Geo,其中bitmaps, HyperLogLong底层是基于String, Geo则是基于zset.
String
介绍:
字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。字符串类型的值实际上可以是字符串(简单的字符串,复杂的字符串(例如JSON,XML)),数字(整数,浮点数),甚至是二进制(图片,音频,视频),但是值最大不能超过512MB。
使用场景:
字符串类型的使用场景很广泛:
缓存功能:
Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
计数
使用Redis作为计数的基础工具,它可以实现快速计数,查询缓存的功能,同时数据可以异步落地的其他数据源。
共享Session
一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。为了解决这个问题,可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
限速
比如,很多应用出于安全的考虑,会在每次进行登陆时,让用户输入手机验证码,从而确定是否用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。一些网站限制一个IP地址不能在一秒钟之内访问超过n次也可以采用类似的思路。
List
介绍:
list 既是 链表
列表(list)类型是用来存储多个有序的字符串,a,b,c,d,e五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element).
列表类型有两个特点:第一,列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表,第二,列表中的元素可以是重复的。
使用场景:
消息队列
Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不是有序的,同时支持按照索引范围获取元素。
实现其他数据结构
lpush + lpop = Stack (栈)
lpush + rpop = Queue(队列)
Ipush + Itrim = Capped Collection(有限集合)
Ipush + brpop = Message Queue(消息队列)
hash
介绍
hash类似于JDK1.8前的HashMap,内部实现也差不多(数组+链表)
使用场景:
1 king 18 boy
使用String类型:
set user:1:name king;
set user:1:age 18;
set user:1:sex boy;
优点:简单直观,每个键对应一个值
缺点:键数过多,占用内存多,用户信息过于分散,不用于生产环境
将对象序列化存入redis
set user:1 serialize(userInfo);
优点:编程简单,若使用反序列化合理内存使用率高
缺点:序列化与反序列化有一定开销,更新属性时需要把userInfo全取出来进行反序列化,更新后再序列化到redis
使用hash类型:
hmset user:1 name king age 18 sex boy
优点:简单直观,使用合理可减少内存空间消耗
缺点:要控制内部编码格式,不恰当的格式会消耗更多内存。
set
介绍
set类似于Java中的HashSet。Redis中的set类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择。
使用场景
set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的,可以基于set轻易实现交集,并集,差集的操作。
集合类型比较典型的使用场景就是标签(tag)。例如一个用户可能对娱乐,体育比较感兴趣,另外一个用户可能对历史,新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。
除此之外,集合还可以通过生成随机数进行比如抽奖活动,以及社交图谱等。
zset
介绍
有序集合相对于哈希,列表,集合来说会有一点点陌生,但既然叫有序集合,那么他和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序,但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。
有序集合提供了
获取指定分数和元素范围查询,计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
使用场景:
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间,按照播放量,按照获得的赞数。
bitmap
介绍:
现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位,例如“big"字符串是由3給字节组成,但实际在计算机存储时将其用二进制表示,"big"分别对应的ASCII码分别是98,105,103,对应的二进制分别是01100010,01101001和01100111。
Bitmaps本身不是一种数据结构,实际上他就是字符串,但是它可以对字符串的位进行操作。Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不同,可以把Bitmaps想象成一个以单位的数组,数组的每个单元只能存储0和1,数组下标在Bitmaps中叫做偏移量。
使用场景:
适合需要保存状态信息(比如是否签到,是否登录。)并需要进一步对这些信息进行分析的场景。比如用户签到情况,活跃用户情况,用户行为统计(比如是否点赞过某个视频)。
24 为什么要用Redis(缓存)?
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变之后,同步改变缓存中的相应数据即可!
高并发
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
25 Redis与Memcached的区别
两者都是非关系型内存键值数据库,现在公司一般都是用Redis来实现缓存,而且Redis自身也越来越强大!Redis与Memcached主要有一下不同:
(1)memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型。
(2)redis的速度比memcached快很多
(3)redis可以持久化其数据
26 Redis的应用场景
计数器
可以对String进行自增自减运算,从而实现计数器功能。Redis这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
会话缓存
可以使用Redis来统一存储多台应用服务器的会话信息。当应用服务器不在存储用户的会话信息,也就不在具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供了一个插件来使用Redis作为全页缓存后端,此外,对WordPress的用户来说,Pantheon有一个非常好的插件wp-redis,这个插件能帮助你以最快的速度加载你曾浏览过的页面。
查找表
例如DNS记录就很适合使用Redis进行存储。查找表和缓存类似,也是利用了Redis快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不能作为可靠的的数据来源。
消息队列(发布/订阅功能)
List是一个双向链表,可以通过lpush和rpop写入和读取消息。不过最好使用Kafka,RabbitMQ等消息中间件。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程同步。可以使用Redis自带的SETNX命令实现分布式锁,除此之外,还可以使用官方提供的RedLock分布式锁实现。
其它
Set可以实现交集,并集等操作,从而实现共同好友等功能。ZSet可以实现有序性操作,从而实现排行榜功能。
27 Redis为什么这么快?
1.完全基于内存,绝大部分请求时纯粹的内存操作,非常快速。数据存在于内存中,类似于HashMap, HashMap的优势就是查找和操作时间的复杂度都是O(1).
2.数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的。
3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多线程或者多线程切换而消耗CPU,不用去考虑各种锁的问题,不存在枷锁释放锁操作,没有因为可能出现的死锁而导致的性能消耗;
4.使用多路I/O复用模型,非阻塞IO;
5.使用底层模型不同,它们之间底层实现方式以及客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
28 为什么要用Redis而不用map/guava做缓存?
缓存分为本地缓存和分布式缓存
以Java为例,使用自带的map或者guava实现的本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持redis或memcached服务的高可用,整个程序架构上比较为复杂。
29 Redis的持久化机制是什么?
Redis的书v就全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证。Redis的数据不会因为故障而丢失,这种机制就是Redis的持久化机制。
Redis的持久化机制有两种,第一种是RDB快照,第二种是AOF 日志。
RDB持久化
RDB持久化是将某个时间点上Redis中数据保存到一个RDB文件中,如下图所示
该文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时Redis中的数据,如下所示
Redis提供了2个命令来创建RDB文件,一个是SAVE,另一个是BGSAVE,因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以推荐使用BGSAVE命令。
载入RDB文件
载入RDB文件的目的是为了在Redis服务器进程重新启动之后还还原之前存储在Redis中的数据。然后,Redis载入RDB文件并没有专门的命令,而是在Redis服务启动时自动执行的。而且,Redis服务器启动时是否会载入RDB文件还取决于服务器是否启用了AOF持久化功能,具体判断逻辑为:
1.只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据。
2.如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来恢复数据。
AOF持久化
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库数据的。如下图所示。
默认情况下,AOF持久化功能是关闭的,如果想要打开,可以修改配置。
载入AOF文件
因为AOF文件包含了重建数据库所需的所有写命令,所以Redis服务器只要读入并重新执行一边,AOF文件里面保存的写命令,就可以还原Redis服务器关闭之前的数据。Redis读取AOF文件并还原数据库的详细步骤如下:
1.创建一个不带网络连接的伪客户端(因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端执行AOF文件保存的写命令。伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样)
2.从AOF文件中分析并读取出一条写命令。
3.使用伪客户端执行被读取出的写命令。
4.一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被执行完毕。
以上步骤如下图所示:
RDB持久化,AOF持久化的区别
实现方式
RDB持久化是通过将某个时间点Redis服务器存储的数据保存到RDB文件中来实现持久化的。
AOF持久化是通过将Redis服务器执行的所有写命令保存到文件中来实现持久化的。
文件体积
由上述实现方式可知,RDB持久化记录的是结果,AOF持久化记录的是过程,所以AOF持久化生成的AOF文件会有体积越来越大的问题,Redis提供了AOF重写功能来减少AOF文件体积。
安全性
AOF持久化的安全性要比RDB持久的安全性高,即如果发生机器故障,AOF持久化要比RDB持久化丢失的数据要少。
因为RDB持久化会丢失上次RDB持久化后写入的数据,而AOF持久化最多丢失1s之内写入的数据(使用默认everysec配置的话)。
优先级
由于上述的安全性问题,如果Redis服务器开启了AOF持久化功能,Redis服务器在启动是会用AOF文件来还原数据,如果Redis服务器没有开启AOF持久化功能,Redis服务器在启动的时会使用RDB文件来还原数据,所以AOF文件的优先级比RDB文件的优先级高。
30 如何保证缓存与数据库双写时的数据一致性?
什么是时数据一致性问题?
只要使用到缓存,无论是本地内存做缓存还是使用redis做缓存,那么就会存在数据同步的问题。
更新缓存方案
1. 先更新缓存,在更新DB
这个方案我们一般不考虑。原因是更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。
2. 先更新DB,再更新缓存
这个方案我们一般不考虑,原因跟第一个一样,数据库跟新成功了,缓存更新失败,同样会出现数据不一致问题。
这种更新缓存的方式还有并发问题。
同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
删除缓存方案
3. 先删除缓存,后更新DB
该方案也会出现问题,具体出现的原因如下。
此时来了两个请求,请求A(更新操作)和请求B(查询操作)
请求A会先删除Redis中的数据,然后去数据库进行更新操作;
此时请求B看到Redis中数据是空的,回去数据库中查询该值,补录到Redis中,
但是此时请求A并没有更新成功,或者事务还未提交,请求B去数据库查询得到旧值。
那么这时候就会缠身数据库和Redis数据不一致的问题。如何解决呢?其实最简单的解决办法就是延迟双删的策略。就是
(1)先淘汰缓存
(2)再写数据库
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
4. 先更新DB,后删除缓存
这种方式,被成为Cache Aside Pattern,读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回相应。更新的时候,先更新数据库,然后再删除缓存。
这种情况下不存在并发问题吗?
依然存在。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会产生如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比写操作,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。一定要解决怎么办?如何解决上述并发问题?
** 首先,给缓存设有效时间是一种方案。**
其次,采用异步延时删除策略。
但是,更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功怎么办?这个问题,在删除缓存类的方案都是存在的,那么此时再读取缓存的时候每次都是错误的数据了。此时解决方案有两个,一个就是利用消息队列进行删除的补偿。具体业务逻辑用逻辑语言描述如下:
- 请求A先对数据库进行更新操作
- 在对Redis进行删除操作的时候发现报错,删除失败
- 此时将Redis的key作为消息体发送到消息队列中
- 系统接收到消息队列发送的消息后
再次对Redis进行删除操作
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对MySQL数据库更新操作后再binlog日志中我们都能够找到相应的操作,那么我们可以订阅MySQL数据库的binlog日志对缓存进行操作。
订阅binlog程序在mysql中有现成的中间件叫canal,阿里开源的,大家可以自行查阅官网,用以完成订阅binlog日志的功能。
31 什么是缓存穿透?怎么解决?
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量请求而崩溃掉。
解决方案
接口层增加效验,如用户鉴权效验,id做基础效验,id <= 0的直接拦截;
从缓存区不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null缓存有效时间可以设置短点,如30秒(设置太长导致正常情况没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
布隆过滤器
采用布隆过滤器,将所有可能存在的书v就哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
1970年布隆提出了一种布隆过滤器的算法,用来判断要给元素是否在一个集合中,这种算法由一个二进制数组和一个Hash算法组成。
选择多个Hash函数计算多个Hash值,这样可以减少误判的几率
32 Redis线程模型,单线程为什么快?
Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器file event handler。这个文件事件处理器,它是单线程的,所以Redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socekt,IO多路复用程序,文件事件分派器以及事件处理器(命令请求处理器,命令回复处理器 ,连接应答处理器等)。
多个Socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个Socekt,会将Socekt放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socekt给对应的事件处理器。
然后一个Socket的事件处理完之后,IO多路复用程序才会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理。
单线程快的原因:
1)纯内存操作
2)核心是基于非阻塞的IO多路复用机制
3)单线程反而避免了多线程频繁上下文切换带来性能问题。
33 Redis的过期键的删除策略
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除,该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况下可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每个一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最有的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定时过期两种策略。
34 缓存雪崩,缓存穿透,缓存击穿
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量过期现象发生。
- 给每一个缓存数据增加相应缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
- 缓存预热
- 互斥锁
缓存穿透是指缓存和数据库中没有的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- 接口层增加效验,如用户鉴权效验,id做基础校验,id<=0的直接拦截
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,有同事去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永远不过期
- 加互斥锁
35 简述Redis事务实现
1. 事务开始
MULTI命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成的。
2. 命令入队
当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来执行不同的操作。如果客户端发送的命令为MULTI,EXEC,WATCH,DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUE回复
- 如果客户端发送的命令为EXEC,DISCARD,WATCH,MULTI四个命令的其中一个,那么服务器立即执行这个命令。
- 如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的flags属性关闭REDIS_MULTI标识,并且返回错误信息给客户端。如果正确,将这个命令放入一个事务队列里面,然后向客户端返回QUEUE回复
事务队列是按照FIFO的方式保存入队的命令。
3. 事务执行
客户端发送EXEC命令,服务器执行EXEC命令逻辑。
- 如果客户端状态的flags属性不包含REDIS_MULTI标识,或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC标识,那么就直接取消事务的执行。
- 否则客户端处于事务状态(flags有REDIS_MULTI标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果去不返回给客户端;
redis不支持事务回滚机制,但是它会检查每个事务中的命令是否错误。
Redis事务不支持检查那些程序员自己逻辑错误。例如对String类型的数据库键执行对HashMap类型的操作!
WATCH命令是一个乐观锁,可以为Redis事务提供check-and-set(CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
MULTI命令用于开启一个事务,他总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC: 执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值nil。
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。
UNWATCH命令可以取消watch对所有key的监控。
36 redis集群方案
主从
哨兵模式:
sentinel, 哨兵是redis集群中非常重要的一个组件,主要有一下功能:
- 集群监控:负责监控redis master和slave进程是否正常工作。
- 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果master node挂掉了,会自动转移到slave node上。
- 配置中心:如果故障转移发生了,通知client客户端新的master地址。
哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举
- 即使部分哨兵节点挂掉了,哨兵集群还能正常工作的
- 哨兵通常需要3个实例,来保证自己的健壮性。
- 哨兵 + redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性。
- 对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点。接收到请求的节点会将查询请求发送到正确的节点上执行。
方案说明
- 通过哈希的方式,将数据分片,每个节点均分布存储一定哈希槽(哈希值)区间的数据,默认分配了16384个槽位
- 每份数据分片会存储在多个互为主从的多节点上
- 数据写如先写主节点,再同步到从节点(支持配置为阻塞同步)
- 同一分片多个节点间的数据不保持强一致性
- 读取数据时,当客户端操作的key没有分配再给节点上时,redis会返回指令,指向正确的节点
- 扩容时需要把旧节点的数据迁移一部分到新节点
在redis cluster架构下,每个redis要放开两个端口号,比如一个时6379,另外一个就是加1W的端口号,比如16379.
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测,配置更新,故障转移授权。cluster bus用了另外一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
优点
- 无中心结构,支持动态扩容,对业务透明
- 具备Sentine的监控和自动Failover(故障转移)能力
- 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
- 高性能,客户端直连redis服务,免去了proxy代理的损耗
缺点 - 运维也很复杂,数据迁移需要人工干预
- 只能使用0号数据库
- 不支持批量操作(pipeline管道操作)
- 分布式逻辑和存储模块耦合等
Redis Shareding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算发将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool
优点
优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强
缺点
由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化。
37 Redis主从复制的核心原理
通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
全量复制:
- (1)主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU,内存(页表复制),硬盘IO的
- (2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗;
- (3)从节点清空旧数据,载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗。
部分复制
- 复制偏移量:执行复制的双发,主从节点,分别会维护一个复制偏移量offfset
- 复制积压缓冲区:主节点内部维护了一个固定长度的,先进先出(FIFO)队列作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
-
- 服务器运行ID(runid):每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点Redis断开重连的时候,就会根据运行ID来判断同步的进度。
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
过程原理:
38 高并发场景秒杀抢购超卖Bug
1.执行过程无法保证原子性,出现脏读。
某一时刻,多个线程同时从Redis中获取到库存值,对库存值进行-1操作,将新的库存值设置进Redis中,会出现脏读。
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
2.synchronized
synchronized关键字,在一个虚拟机中可以保证代码块串行化执行,如果项目部署到多台服务器上,并不能保证代码快执行串行化,引发脏读问题。
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
synchronized (this){//分布式项目,多台服务器部署,有问题,锁无效
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
3. setnx实现分布式锁
SETNX
格式:setnx key value
将key的值设为value,当且仅当key不存在。
若给定的key已经存在,则SETNX不做任何动作。
SETNX是[set if not exists] (如果不存在,则SET)的简写
可用版本:
>= 1.0.0
这种是利用Redis中setnx命令做分布式锁。如果在执行减库存的过程中,抛出异常,那么就会出现Redis中stock将无法被删除,出现死锁问题。
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "lock:product:101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");//jedis.setnx("localKey", "zhuge");
if(!result){
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
stringRedisTemplate.delete(lockKey);
return "end";
}
4. 解决逻辑中的异常问题
设置try/catch,如果在减库存过程中抛出异常,将删除锁放到finally中,确实能够将锁删除;如果在减库存的过程中服务器宕机,依然不能够将所删除。
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "lock:product:101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");//jedis.setnx("localKey", "zhuge");
if(!result){
return "error_code";
}
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
5. 设置锁自动删除时间
通过设置锁的自动删除时间,避免出现其他线程一直获取不到锁
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "lock:product:101";
// Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");//jedis.setnx("localKey", "zhuge");
// stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS); //存在原子安全问题
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge", 10, TimeUnit.SECONDS);
if(!result){
return "error_code";
}
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
6. 删除不是自己加的锁
1.a线程加了锁,业务逻辑执行超过10s(有可能是因为网络震荡,其他原因),Redis中的锁已经过期删除了;
2.b线程成功加了锁,执行业务逻辑,这时a线程执行完毕业务逻辑,要删除锁,删掉的就是b线程的锁;
3.b线程的锁删除完毕后,其他线程又来加锁,b执行完业务逻辑就去删除锁,这时删除的锁,是其他线程的锁,并非b线程添加的锁。
4.造成删除锁混乱,破坏原子性。
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "lock:product:101";
String clientId = UUID.randomUUID().toString();
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if(!result){
return "error_code";
}
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
//判断是当前线程加的锁和删除锁这两个操作不是原子性的,还是有可能出问题,有可能出现判断完毕以后,删除的锁不是自己加的锁
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
7. 锁续命
Redisson实现锁续命
Redisson加锁实现原理图:
<!-- redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.4</version>
</dependency>
@Bean
public Redisson redisson(){
//单机模式配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.65.100:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock(){
String lockKey = "lock:product:101";
RLock redissonLock = redisson.getLock(lockKey);
redissonLock.lock();//加锁
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); //jedis.set(key, value);
System.out.println("扣减成功,剩余库存:" + realStock);
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
redissonLock.unlock();
}
return "end";
}
一个线程抢到资源,其他线程在等待资源的释放,如何设计性能比较高?
其他线程等待资源的释放,一定要轮询尝试看能不能获取到锁。
8. Redlock实现原理
RedissonLock这种模式也有可能存在问题?
Redis master节点被线程1加锁成功,还没来得及将锁数据同步到slave节点,master节点宕机,slave被推举成为master节点。新master节点中并没有线程1加的锁数据,导致锁丢失。其他线程也能成功加锁,不能保证原子性。
使用RedLock来解决:
RedLock加锁成功需要,集群中一般以上的机器加锁成功,才算加锁成功。