分布式锁

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

为什么要使用分布式锁

在单机环境下,当存在多个线程可以同时改变某个变量(可变共享变量)时,就会出现线程安全问题。这个问题可以通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等来避免。

而在多机部署环境,需要在多进程下保证线程的安全性,Java提供的这些API仅能保证在单个JVM进程内对多线程访问共享资源的线程安全,已经不满足需求了。这时候就需要使用分布式锁来保证线程安全。通过分布式锁,可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

# 分布式锁应该具备哪些条件

在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会死锁。具备锁失效机制,防止死锁。即使有客户端在持有锁的期间崩溃而没有主动解锁,也要保证后续其他客户端能加锁。
  3. 加锁和解锁必须是同一个客户端。客户端a不能将客户端b的锁解开,即不能误解锁。
  4. 高性能、高可用的获取锁与释放锁。
  5. 具备可重入特性。
  6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

# 分布式锁的三种实现方式

  1. 基于数据库实现分布式锁;
  2. 基于缓存(Redis等)实现分布式锁;
  3. 基于Zookeeper实现分布式锁。

# 基于数据库的实现方式

悲观锁

创建一张锁表,然后通过操作该表中的数据来实现加锁和解锁。当要锁住某个方法或资源时,就向该表插入一条记录,表中设置方法名为唯一键,这样多个请求同时提交数据库时,只有一个操作可以成功,判定操作成功的线程获得该方法的锁,可以执行方法内容;想要释放锁的时候就删除这条记录,其他线程就可以继续往数据库中插入数据获取锁。

乐观锁

每次更新操作,都觉得不会存在并发冲突,只有更新失败后,才重试。

扣减余额就可以使用这种方案。

具体实现:增加个version字段,每次更新修改,都会自增加一,然后去更新余额时,把查出来的那个版本号,带上条件去更新,如果是上次那个版本号,就更新,如果不是,表示别人并发修改过了,就继续重试。

# 基于Redis的实现方式

# 简单实现

Redis 2.6.12 之前的版本中采用 setnx + expire 方式实现分布式锁,在 Redis 2.6.12 版本后 setnx 增加了过期时间参数:

SET lockKey value NX PX expire-time

所以在Redis 2.6.12 版本后,只需要使用setnx就可以实现分布式锁了。

加锁逻辑:

  1. setnx争抢key的锁,如果已有key存在,则不作操作,过段时间继续重试,保证只有一个客户端能持有锁。
  2. value设置为 requestId(可以使用机器ip拼接当前线程名称),表示这把锁是哪个请求加的,在解锁的时候需要判断当前请求是否持有锁,防止误解锁。比如客户端A加锁,在执行解锁之前,锁过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
  3. 再用expire给锁加一个过期时间,防止异常导致锁没有释放。

解锁逻辑:

首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁。使用lua脚本实现原子操作,保证线程安全。

下面我们通过Jedis(基于java语言的redis客户端)来演示分布式锁的实现。

Jedis实现分布式锁

查看更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

滚动至顶部