分布式锁概念
在多线程环境下,为了保证数据的线程安全,锁保证同一时刻,只有一个可以访问和更新共享数据。在单机系统我们可以使用 synchronized 锁、Lock 锁保证线程安全。
synchronized 锁是 Java 提供的一种内置锁,在单个 JVM 进程中提供线程之间的锁定机制,控制多线程并发。只适用于单机环境下的并发控制。
想要在多个节点中提供锁定,在分布式系统并发控制共享资源,确保同一时刻只有一个访问可以调用,避免多个调用者竞争调用和数据不一致问题,保证数据的一致性,就需要分布式锁。
分布式锁:控制分布式系统不同进程访问共享资源的一种锁的机制。不同进程之间调用需要保持互斥性,任意时刻,只有一个客户端能持有锁。
共享资源包含:
数据库
文件硬盘
共享内存
分布式锁特性:互斥性:锁只能被持有的客户端删除,不能被其他客户端删除
锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁
可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效
安全性:锁只能被持有的客户端删除,不能被其他客户端删除
创建锁
使用Redis实现分布式锁可以通过setnx(set if not exists)命令实现,但当我们使用setnx创建键值成功时,则表中加锁成功,否则代码加锁失败,实现示例如下:
127.0.0.1:6379> setnx lock true
(integer) 1#创建锁成功
#逻辑业务处理..
当我们重复加锁时,只有第一次会加锁成功
127.8..1:6379> setnx lock true # 第一次加锁
(integer) 1
127.8.8.1:6379> setnx lock true # 第二次加锁
(integer) 0
从上述命令可以看出,我们可以看执行结果返回是不是1,就可以看出是否加锁成功
释放分布式锁
127.0.0.1:6379> de1 lock
(integer) 1 #释放锁
然而,如果使用 setnx ock true 实现分布式锁会存在死锁问题,以为 setnx 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况
解决死锁问题
死锁问题可以通过设置超时时间来解决,如果超过了超时时间,分布锁会自动释放,这样就不会存在死锁问题了也就是 setnx和 expire 配合使用,在 Redis 2.6.12 版本之后,新增了一个强大的功能,我们可以使用一个原子操作也就是一条命令来执行 setnx 和expire 操作了,实现示例如下:
127.0.0.1:6379> set lock true ex 3 nx
OK #创建锁成功
127...1:6379> set lock true ex 3 nx
(ni1) #在锁被占用的时候,企图获取锁失败
其中ex为设置超时时间, nx 为元素非空判断,用来判断是否能正常使用锁的。
因此,我们在 Redis 中实现分布式锁最直接的方案就是使用 set key value ex timeout nx 的方式来实现
超卖问题
这是因为在并发环境下,多个线程下单操作,前面的线程还未更新库存,后面的线程已经请求进来,并获取到了未更新的库存,后续扣减库存都不是扣减最近的库存。线程越多,扣减的库存越少。这就是在高并发场景下发生的超卖问题。
很明显,上述问题是出现了线程安全的问题,我们首先能想到的肯定是给它加 synchronized 锁。
是的,没问题,但是我们知道,synchronized 锁是属于JVM 级别的,也就是我们所谓的“单机锁”,如果是多机部署的环境中,还能保证数据的一致性吗?
答案肯定是不能的。这个时候,就需要用到了我们 Redis 分布式锁
用 Redis 实现分布式锁的几种方案,都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况