redis分布式锁踩坑记录
前言
下面说的问题, 很多都可以使用redisson
解决, 所以我就不说redisson
了
我就说说不用redisson
如何解决吧
本章只提供思路
redis
主从复制的坑
问题
redis 主从复制存在时间间隔, 主节点写入数据之后, 从节点还没来得及同步主节点数据.
主节点挂了, 从节点没有该数据, 此时从节点变身为主节点, 完了, 数据没有, 原本主节点的分布式锁丢失
其他节点可以去加锁了
解决
使用redisson
的RedLock
, 或者使用redissonClient.getMultiLock
RedLock
的思路非常简单, 给3个节点添加分布式锁, 如果两个节点已经有了锁数据, 就表示上锁成功(大于节点一般的数)
后面思路就简单了, 如果某个主节点挂了, 剩下的两个主节点都存在分布式锁, 即便从节点变成主节点, 还是可以保证分布式锁还锁着
但是这种方式已经被
redisson
标记最好不要再使用了
还可以使用多个锁, 这种方式更加简单, 需要全部节点都写入才算写入分布式锁, 如果有一个节点错误那么分布式锁失败, 就这么简单
存在问题
效率极慢
redis
分布式锁不可重入
问题: 不可重入问题
默认情况redis
使用setnx
实现分布式锁, 但是这种锁不可重入, 无法重试
解决方案
锁不可重入的原因在于, 重入锁的方案是上一次锁, 计数一次, 解锁一次, 计数减少一次, 直到计数为0, 那么锁删除
所以我们不能使用string类型的锁, 我们可以使用 hash
key的话类似于对象锁, value的话使用的是 uuid + threadId形式
uuid区分是否是同一个项目, 一般是静态类型
这个uuid通常是java的UUID类型是静态的类型生成的一个微服务就只有一个唯一的
问题: 锁超时问题
问题发现
如果锁超时请求还未执行完毕, 那么下一次又有新的请求在上锁, 此时正在执行的线程至少两个以上
解决方法
锁超时问题其实也很好解决, 续约
锁在上锁的时候, 添加定时器, 在锁即将过期前, 如果还没有解锁的话, 我们可以续约时间
传入参数: 锁超时时间, 默认锁续约总时间
创建定时器, 在锁过期的时候续约
事务和锁作用域不同
问题
事务和锁的作用域不同导致锁解锁了, 但是事务还未提交, 此时又来了个线程2上了锁, 而线程1的线程还未交事务提交掉
解决方案
让锁的作用域大于事务的
实现方法两个 TransactionTemplate
或者使用 AOPContext
获取代替对象 调用添加了事务注解的函数
一人参与一次秒杀
问题发现: 一人多次秒杀
正常一个人只能参与一次秒杀
但是很多人使用外挂, 实现一人在短时间内发生多次秒杀的情况, 这种是不行的
解决方案
非常简单, 分布式Set集合就行了
订单id是key, set集合是userId, 在插入集合时判断下set集合有多少数据了, 达到商品数量就可以返回秒杀结束
这里面还有多个步骤导致线程不安全的问题
秒杀的其他解决方案:
要对10000包奶粉进行秒杀 那么你就可以给多个集群分发数量进行秒杀 比如我们的集群有3台, 那么我可以使用限流组件将1w包奶粉分发给 3 台电脑, 两台 3000 包, 其中一台 4000 包, 在这三台服务器之前放置一个类似负载均衡的机器, 如果有一台服务器中的奶粉被秒杀结束, 那么告知负载均衡组件, 负载均衡组件对其进行降级
redis
连接池用尽
存在redis
连接池连接被用尽, 导致无法获取新的redis
连接, 特别是你开启了redis
事务的情况下
解决方案: 使用RedisConnectionUtils
释放redis
连接
stringRedisTemplate.connectionFactory?.let { RedisConnectionUtils.unbindConnection(it) }
Thread.sleep(20)
删除了别人的锁
分布式锁存在最初始版本有点像
我上了A锁, 你发现A锁在, 那我就等着
现在我上了A锁, 然后我被阻塞住了, A锁超时, 你发现没锁了, 直接上A锁
此时我开始运行, 完成任务, 执行 unlock, 此时把A锁解锁掉了
解决方案
这不是很简单, 给锁写上我的名字, 但是我的名字很多, 也许存在同名的我, 所以再加上我家的地址
我的名字: threadId, 我家的地址: static final String uuid = UUID.toString()
静态保证了整个微服务的标记, 也就是我家的住址, 而我家有多个不重名的成员
unlock涉及多个步骤线程不安全
解决方案
使用lua脚本
秒杀业务性能提升
秒杀业务由于存在无限个人秒杀商品, 所以外界的压力是无限大的
那么我们只能使用同样看起来无限大的redis去承载外部的无限大的秒杀任务
将秒杀和数据库隔开, 这样保证数据库不会收到过量的IO操作
所以秒杀操作全部放在redis中
秒杀任务触发时, 将会包装一个event事件丢给mq消息队列
消息队列将会以mysql能够受得了的频率慢慢写入数据
为什么这里要使用mq, 而不是线程池呢? 因为线程池太容易丢失event了, 特别是断电了怎么办? 它的阻塞队列存在于内存中的
转载自:https://juejin.cn/post/7229377777916559419