typora-copy-images-to: imgs
Redisson 分布式锁?在项目中哪里使用?多久会进行释放?如何加强一个分布式锁?
答:
首先入门级别的分布式锁是通过 setnx
进行实现,使用 setnx
实现有四个注意点
需要设置锁的超时时间(如果不设置,在释放锁时,如果机器宕机,会导致锁无法释放)
需要设置一个唯一 ID,表示这个锁是哪个用户添加的,必须由添加锁的用户释放
(如果不设置,线程1在执行任务时,可能锁的超时时间已经达到,被自动释放,此时线程2加锁,开始执行业务,但正好线程1执行完毕,释放锁,由于没有唯一ID表示,线程1将线程2加的锁给释放掉了)
需要锁续命
有可能锁的过期时间设置的太短,导致业务没有执行完毕,锁就被自动释放,因此要使用锁续命来解决(大概逻辑是使用子线程执行定时任务,定时任务间隔时间要小于 key 的过期时间,子线程隔一段时间判断主线程是否在执行,如果在执行,就重新设置一下过期时间)
可重入问题:setnx 实现的分布式锁不可重入,这样获取锁的线程在重复进入相同锁的代码块中会造成死锁
而在 Redission 中已经帮我们实现好了分布式锁,下来看一下 Redission 中的分布式锁:
Redission 中获取锁逻辑:
在 Redission 中加锁,通过一系列调用会到达下边这个方法
他的可重入锁的原理也就是使用 hash 结构来存储锁,key 表示锁是否存在,如果已经存在,表示需要重复访问同一把锁,会将 value + 1,即每次重入一次 value 就加 1,退出一次 value 就减 1
下列方法有三个参数分别为:
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.
Redission 中锁续命原理:
Redission 底层有个看门狗机制,加锁成功后会有一个定时任务,默认锁的失效时间是 30s,该定时任务每隔锁失效时间的 1/3 就会去续约锁时间,也就是每隔 10s 进行锁续命
如何加强一个分布式锁?
也就是如何提升一个分布式锁的性能,分布式锁本质上是将并行操作改为串行,那么我们可以通过使用分段锁
来提升性能,比如说有 1000 个库存的话,读入到缓存中将分为 10 份进行存储,即 product_stock_1 = 100, product_stock_2 = 100, ...
,给每一份都加上所,那么多个线程来竞争这 10 把锁,比原来竞争 1 把锁的性能提高 10 倍
zset 的底层实现?为什么不用红黑树?
答:
zset 的底层实现是:压缩列表 + 跳表
什么时候使用压缩列表?
否则使用跳表
跳表中每个节点都有多个跳跃指针,因此每个节点的平均跳跃长度较长,可以一次跳过多个节点,当找到大于或等于目标元素的节点后,再使用普通指针开始移动(可以向后移动,也可以向前移动,跳表含有前边节点的指针)寻找目标元素,跳表可以在 O(logn)
的时间内遍历跳表
跳表结构图:
为什么不用红黑树?
跳表和红黑树的查找时间复杂度都是O(logn)
,但是红黑树比跳表的插入/删除效率更低
跳表实现比红黑树更简单
zset 几个命令的时间复杂度?
答:
zadd
:O(logn),添加一个元素的时间复杂度是 O(logn)(因为插入元素的话,时间开销都在查找插入位置上,在 zset 中,查找时间复杂度是 O(logn),因此插入复杂度同是)zrange
:O(logn + m) ,n 是集合中元素数量,m 是指定范围内的用户数量redis 里面的命令,比如 setnx 和 setex 还有 zset 中的命令?
答:
zset 中的常用命令为:
zadd
向集合 key 中添加元素
zrange
查找下标在 start 和 stop 之间的元素,如果后边带上 withscores
参数,会将分数也查询出来
zrevrange
将分数从大到小进行查询,和 zrange 查询顺序相反
zrangebyscore
返回集合 key 中所有 score 介于 min 和 max 之间的成员,如果后边带上 withscores
参数,会将分数也查询出来
setex
设置 key、value 并且设置过期时间
setnx
仅当 key 不存在时,才将 key 的值设置为 value,成功返回1,失败返回0
缓存怎么保证数据的一致性?
答:
先删除缓存,再更新数据库
(操作简单)
这种情况造成的缓存不一致为:线程 A 先删除缓存,再去更新数据库,在线程 A 更新数据库之前,如果线程 B 去读取缓存,发现并不存在,去读取数据库,此时读取的是旧数据,再将旧数据写入缓存,此时缓存存储的就是脏数据了。
使用更新数据库 + 延时双删
可以解决此情况的数据不一致,在延时双删中,会删除两次缓存,分为以下几步:
1. 删除缓存
2. 更新数据库
3. 睡眠 Thread.sleep()
4. 再删除缓存
即延时双删在线程 A 更新完数据库之后,休眠一段时间,再去删除缓存中可能存在的脏数据。
这样第二次删除缓存可能导致线程 A 执行时间过长,第二次可以使用异步
去删除缓存
先更新数据库,再删除缓存
这种情况可能因为线程 A 没有及时删除缓存或者删除缓存失败而导致线程 B 读取到旧数据
可以使用消息队列来完成:
高级扩展点:Canal 监听日志更新 + 定时任务缓存处理
,简单概括来说就是 Canal 可以监控 MySQL 的 binlog,当发现数据库的数据发生变化后,就去同步缓存,就可以达到最终的数据一致性了
redis基本数据结构?
答:
有 5 中基本数据结构:字符串、list列表、hash字典、set集合、zset有序集合
基本数据结构添加数据命令:
字符串: set key value [ex seconds | px milliseconds] [nx | xx]
nx:指定 key 不存在才会设置成功
xx:指定的 key 必须存在才会设置成功,用于更新 key
list:lpush key value [value...]
rpush key value [value...]
hash: hset key field value
将 key 中的 field 的值设置为 value
set: sadd key value [value...]
zset:zadd key score value
向 key 中添加一个 value 和 score,根据 score 排序
数据结构基本介绍:
基本数据结构应用场景:
字符串
限速器:防止 DoS 攻击,对 ip 进行访问次数限制,但是无法防止 DDoS 攻击,因为 DDoS 是分布式拒绝服务,使用了不同 ip 不断访问服务器
// 等价于 set 192.168.55.1 ex 60 nx
// 如果该ip不存在,指定key为ip,value为1,过期时间为60秒
Boolean exists = redis.set(ip, 1, "ex 60", "nx");
if(exists != null || redis.incr(ip) <= 5) {
// 通过访问
} else {
// 限流
}
list
lpush + lpop
实现rpush + lpop
或 lpush + rpop
实现lpush + brpop
实现hash
set
zset