分布式锁
分布式锁的应用场景#
在传统单机部署的情况下,可以使用Java并发处理相关的API(如synchronized)进行互斥控制。
但是在分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机并发控制锁策略失效,A 服务器上的 synchronized
并不能限制 B 服务器的程序,所以仅靠关键字无法解决分布式系统的线程并发问题
Redis实现分布式锁#
一个简单的实现#
Redis中有一个命令SETNX key value
,这个命令的作用是set if not exists
,这条指令执行的过程中,当key
不存在时,设置value
,当其存在的时候,则什么也不做。
有了这条命令,再结合Redis的单线程的特性,可以得出以下解决方式
当一个服务器成功的向 Redis 中设置了该命令,那么就认定为该服务器获得了当前的分布式锁,而其他服务器此时就只能一直等待该服务器释放了锁为止。
那么一个商品秒杀下的情景用代码可以实现成
// 为了演示方便,这里简单定义了一个常量作为商品的id
public static final String PRODUCT_ID = "100001";
public String deductStock() throws InterruptedException {
// 通过 stringRedisTemplate 来调用 Redis 的 SETNX 命令,key 为商品的id,value的值在这不重要
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(RODUCT_ID, "jojo");
if (!result) {
return "error";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int readStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + readStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
// 业务执行完成,删除PRODUCT_ID key
stringRedisTemplate.delete(PRODUCT_ID);
return "end";
}
释放锁#
上面这种实现方式先然还存在着很大的问题,比如:当程序成功拿到锁,并且执行完业务逻辑,却没有安全的执行到stringRedisTemplate.delete(PRODUCT_ID)
,也就是没有成功的释放锁,就会进入死锁状态
或许会下意识想到,可以通过try-finally
语法块去解决,将释放锁的语句放进finally
语法块中。这种解决方式在学习中可能是个好的办法,但是实际生产中不可控因素很多,比如:当线程在成功加锁之后,执行业务代码时,还没来得及删除 Redis 中的锁标志,此时,这台服务器宕机了,程序并没有想我们想象中地去执行 finally
块中的代码。这种情况也会使得其他服务器或者进程在后续过程中无法去获取到锁,从而导致死锁,最终导致业务崩溃的情况。
Redis超时机制#
当我们在锁机制中调用Redis存活时间的设置
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
依然可能会产生死锁的问题,因为加锁以及设置过期时间是分开来执行的,并不能保证原子性。所以为了解决这个问题,Redis 中也提供了将设置值与设置过期时间合一的操作
stringRedisTemplate.opsForValue().opsForValue().setIfAbsent(lockKey, "jojo", 10, TimeUnit.SECONDS);
这样解决了原子性的问题,但是如果业务代码在10秒内没有执行完,锁却被释放掉,这种问题该如何解决?这块先留个伏笔,我们先将代码进行抽取,方便后续的修改
代码抽取#
RedisLock接口
public interface RedisLock {
/**
* 尝试加锁
*/
boolean tryLock(String key, long timeout, TimeUnit unit);
/**
* 解锁操作
*/
void releaseLock(String key);
}
接下来,基于已有思路来实现这个接口
public class RedisLockImpl implements RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit) {
return stringRedisTemplate.opsForValue().setIfAbsent(key, "jojo", timeout, unit);
}
@Override
public void releaseLock(String key) {
stringRedisTemplate.delete(key);
}
}
进一步优化,“加锁&解锁”归一化#
上面的代码,除了说过的超时问题没有解决,还有一个更大的问题:其他开发人员有可能在编写代码的时候并没有调用 tryLock()
方法,而是直接调用了 releaseLock()
方法,并且可能在调用 releaseLock()
时传入的 Key 值与你调用 tryLock()
时传入的 Key 值是相同的,那么此时就可能出现问题,另一段代码在运行时,硬生生将你代码中加的锁给释放掉了,那么此时的锁就失效了。面对这个问题,我们不得不对加锁和解锁的操作进行归一化处理。所谓的归一化,就是加锁和解锁的线程必须为同一线程,也就是我的锁不能让老王解开
为了进行归一化的操作,需要借助ThreadLocal
和UUID
ThreadLocal叫本地线程变量,尽管名字中带有“Thread”,但是它是一个变量而非线程,所以称其为“ThreadLocalVariable”可能更易于理解,其支持泛型,并提供了set和get方法,顾名思义就是其中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,这为解决避免他人释放我们的锁提供了一种很好的思路
结合ThreadLocal和UUID可以得到以下代码
public class RedisLockImpl implements RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLock<string> threadLock = new ThreadLock<>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit) {
String uuid = UUID.randomUUID().toString();
threadlocal.set(uuid);
return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}
@Override
public void releaseLock(String key) {
if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))) {
stringRedisTemplate.delete(key);
}
}
}
再次优化,自旋锁#
在上面的代码中,我们保证了锁不可以被两个人同时拥有,当我们一次性获取到锁,那么就会直接返回失败,这对业务来说是十分不友好的,假设用户此时下单,刚好有另外一个用户也在下单,而且获取到了锁资源,那么该用户尝试获取锁之后失败,就只能直接返回“下单失败”的提示信息的。所以我们需要实现以自旋的形式来获取到锁,即不停的重试
public class RedisLockImpl implements RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit) {
Boolean isLocked = false;
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
// 尝试获取锁失败,则自旋获取锁直至成功
if (!isLocked) {
for (;;) {
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
if (isLocked) {
break;
}
}
}
return isLocked;
}
@Override
public void releaseLock(String key) {
// 判断当前线程所对应的uuid是否与Redis对应的uuid相同,再执行删除锁操作
if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))) {
stringRedisTemplate.delete(key);
}
}
}
超时优化#
解决超时问题的思路其实很简单:如果程序没有执行完业务代码,就给锁续费。尽管思路简单,但是如果自己造轮子还是比较麻烦的,好在已经有了现成工具供我们使用:Redission。
Redission的看门狗(WatchDog)机制:在redisson实例被关闭前,会不断的延长锁的有效期,如果redisson实例被关闭了,那么锁时间到后自动关闭
看门狗机制一举两得的解决了两个问题:分布式锁超时、程序中途宕机导致的死锁
以下为Redission解决锁超时的一个简单实例
pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.1</version>
</dependency>
Redission配置
@Configuration
public class redissonConfig {
@Bean
public RedissonClient configRedisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://172.0.0.1:6379");
//Redis键-值编解码器。默认为json编解码器
config.setCodec(new StringCodec());
//设置看门狗的时间,不配置的话默认30000
//每 time/3 时进行一次检测
config.setLockWatchdogTimeout(12000);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
业务代码
@Autowired
RedissonClient redisson;
@GetMapping(value = "/getWatchDogLock")
public String getWatchDog() throws InterruptedException {
RLock watchDogLock = redisson.getLock("watchDogLock");
boolean judge;
//进行3s的尝试时间,如果失败则返回false, 还可以设置锁过期时间,如果设置会导致看门狗无效
judge = watchDogLock.tryLock(3 , TimeUnit.SECONDS);
//输出是否能够获取到锁
System.out.println(Thread.currentThread().getName() + "的锁获取情况:" + judge);
//如果获取到锁
if (judge){
try {
System.out.println(Thread.currentThread().getName() +
" 已经成功获取到的分布式锁" +
System.currentTimeMillis());
//执行主要业务逻辑
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//最后进行解锁操作,如果服务宕机,则到不了这一步,会等待看门狗监控锁结束锁清除
System.out.println(Thread.currentThread().getName() +
" 进行解锁操作" + System.currentTimeMillis());
watchDogLock.unlock();
}
}else {
//如果没有获取到分布式锁,执行业务逻辑
System.out.println(Thread.currentThread().getName() +
" 没有成功获取到分布式锁,锁已经被占用" + System.currentTimeMillis());
}
//最后输出
if (judge){
return "成功获取到分布式锁";
}
return "没有获取到分布式锁";
本篇内容参考
《【Redis】利用Redis实现分布式锁》
《Redis分布式锁业务没执行完但锁超时了怎么办? --Redisson》
作者:colee51666
出处:https://www.cnblogs.com/colee51666/p/16433280.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
尊重每一个原创,从你我开始!
转载请注明原文链接,如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“👍”哦,博主在此感谢你的支持!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端