Skip to content

cadecode/distributed-lock-demo

Repository files navigation

distributed-lock-demo

利用数据库、Redis 实现分布式锁 demo

分布式锁简介

在单机环境的 Java 编程中,我们经常使用 JDK 提供的 synchronized 关键字、ReentrantLock 类 等 API 来对共享资源进行加锁,以此保证多线程访问数据的正确性

随着用户需求的不断扩大,加上软件技术的更新迭代,分布式架构和集群技术越来越流行,那么, 在多机环境下,如何保证多台机器上代码的互斥执行,这显然不是单机线程之间的锁可以解决的, 因此分布式锁应运而生

分布式锁有许多实现方案,目前较为流行且结构简单的分布式锁一般采用 Redis 实现

Database(FOR UPDATE) 实现

使用数据库实现分布式锁一般性能较差,在大量并发情况下难以支撑,代码仅用来理解分布式锁的思想

基本过程

  1. 开启一个事务,执行 FOR UPDATE 查询
  2. 查询结果为空,就插入一条锁记录,再次 FOR UPDATE 查询
  3. FOR UPDATE 锁行成功的线程,获取执行机会
  4. 释放锁时提交事务即可

需要注意的是,开启事务时,需要先获取一个单独的数据库连接对象已完成后续操作,如果使用 Spring 提供的事务管理器,可能会影响使用锁时业务上事务的使用

因为事务具有线程吸附性,在 Spring 事务管理器或者声明式事务中,事务是存放在 TreadLocal 中 进行线程内共享的,如果分布式锁不走单独的数据连接,可能会使加锁解锁之间的业务代码被解锁操作提交, 从而产生意料不到的错误

非阻塞加锁

  1. 使用 FOR UPDATE NO WAIT 代替 FOR UPDATE 查询
  2. FOR UPDATE 锁行失败就报错
  3. 一段时间内循环 FOR UPDATE NO WAIT 操作,实现带超时时间的非阻塞加锁

Redis(SETNX) 实现

Redis SETNX 命令表示不存在才添加,此命令是实现分布式锁的基石

基本过程

  1. 使用 SETNX 命令设置一个键
  2. 设置成功则表示获取锁,失败则表示没抢到锁
  3. 循环上述操作,实现阻塞加锁

非阻塞加锁

  1. SETNX 命令本身就是非阻塞的,设置失败就直接返回
  2. 一段时间内循环 SETNX 操作,实现带超时时间的非阻塞加锁

存在的问题

可重入性问题

  1. 可重入锁是指可递归调用的锁,在外层使用锁之后,内部再次使用,不会死锁,而是正常获得锁
  2. 分布式锁基本实现思路有两种:
    • 获取锁后将本地信息保存到数据库或 Redis,重入和释放锁时查询信息进行比对
    • 维护一个 ThreadLocal,保存加锁信息,重入和释放时取出进行判断

容灾问题

  1. 如果服务器宕机,锁没有及时释放,会造成其他线程长时间阻塞,解决方法:
    • 如果基于数据库 FOR UPDATE 实现,一般断开连接后,数据库会自动释放锁,也可以手动在数据库层面将锁解除
    • 如果是 Redis 实现,可以在 SETNX 命令加入过期时间,并开启守护线程为其续期,这样就算宕机也能保证锁在短时间内失效
  2. Redis 集群发生主从切换,在同步数据时可能发生异常,导致锁没有同步成功,其他线程可能也会加锁成功,可以参考 RedLock 的实现
  3. 一般业务中,推荐使用中间件 Redisson,其方案更加成熟,并基于 Netty 实现消息订阅来避免无效轮询,阻塞锁的性能更好

About

利用数据库、Redis 实现分布式锁 demo

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages