利用数据库、Redis 实现分布式锁 demo
在单机环境的 Java 编程中,我们经常使用 JDK 提供的 synchronized 关键字、ReentrantLock 类 等 API 来对共享资源进行加锁,以此保证多线程访问数据的正确性
随着用户需求的不断扩大,加上软件技术的更新迭代,分布式架构和集群技术越来越流行,那么, 在多机环境下,如何保证多台机器上代码的互斥执行,这显然不是单机线程之间的锁可以解决的, 因此分布式锁应运而生
分布式锁有许多实现方案,目前较为流行且结构简单的分布式锁一般采用 Redis 实现
使用数据库实现分布式锁一般性能较差,在大量并发情况下难以支撑,代码仅用来理解分布式锁的思想
- 开启一个事务,执行 FOR UPDATE 查询
- 查询结果为空,就插入一条锁记录,再次 FOR UPDATE 查询
- FOR UPDATE 锁行成功的线程,获取执行机会
- 释放锁时提交事务即可
需要注意的是,开启事务时,需要先获取一个单独的数据库连接对象已完成后续操作,如果使用 Spring 提供的事务管理器,可能会影响使用锁时业务上事务的使用
因为事务具有线程吸附性,在 Spring 事务管理器或者声明式事务中,事务是存放在 TreadLocal 中 进行线程内共享的,如果分布式锁不走单独的数据连接,可能会使加锁解锁之间的业务代码被解锁操作提交, 从而产生意料不到的错误
- 使用 FOR UPDATE NO WAIT 代替 FOR UPDATE 查询
- FOR UPDATE 锁行失败就报错
- 一段时间内循环 FOR UPDATE NO WAIT 操作,实现带超时时间的非阻塞加锁
Redis SETNX 命令表示不存在才添加,此命令是实现分布式锁的基石
- 使用 SETNX 命令设置一个键
- 设置成功则表示获取锁,失败则表示没抢到锁
- 循环上述操作,实现阻塞加锁
- SETNX 命令本身就是非阻塞的,设置失败就直接返回
- 一段时间内循环 SETNX 操作,实现带超时时间的非阻塞加锁
- 可重入锁是指可递归调用的锁,在外层使用锁之后,内部再次使用,不会死锁,而是正常获得锁
- 分布式锁基本实现思路有两种:
- 获取锁后将本地信息保存到数据库或 Redis,重入和释放锁时查询信息进行比对
- 维护一个 ThreadLocal,保存加锁信息,重入和释放时取出进行判断
- 如果服务器宕机,锁没有及时释放,会造成其他线程长时间阻塞,解决方法:
- 如果基于数据库 FOR UPDATE 实现,一般断开连接后,数据库会自动释放锁,也可以手动在数据库层面将锁解除
- 如果是 Redis 实现,可以在 SETNX 命令加入过期时间,并开启守护线程为其续期,这样就算宕机也能保证锁在短时间内失效
- Redis 集群发生主从切换,在同步数据时可能发生异常,导致锁没有同步成功,其他线程可能也会加锁成功,可以参考 RedLock 的实现
- 一般业务中,推荐使用中间件 Redisson,其方案更加成熟,并基于 Netty 实现消息订阅来避免无效轮询,阻塞锁的性能更好