文章目录
4.分布式锁
4.1基本原理和不同实现方式对比
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

特点:
- 多线程可见
- 互斥
- 高可用
- 高性能
- 安全性
分布式锁的常见实现方式:

4.2Redis的分布式锁实现思路
实现分布式锁需要两个基本方法:
-
获取锁
-
互斥:确保只能有一个线程使用锁
shell# 添加锁 set [key] [value] ex [time] nx
-
-
释放锁
-
手动释放
shell# 释放锁(除了使用del手动释放,还可超时释放) del [key] -
超时释放:获取锁时添加一个超时时间
-

4.3实现Redis的分布式锁
初级版本:定义一个类,实现接口,利用Redis实现分布式锁
java
package com.hmdp.utils;
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期自动释放
* @return true表示获取成功
*/
boolean tryLock(Long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
1.创建分布式锁
java
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private String name;//锁的名称
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程表示
long id = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id + "", timeoutSec, TimeUnit.SECONDS);
//Boolean到boolean自动拆箱可能会有空指针问题,所以要判断一下
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
2.使用分布式锁
删去之前的synchronized,使用基于Redis实现的分布式锁
java
Long userId = UserHolder.getUser().getId();
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate,"order:" + userId);
boolean isLock = lock.tryLock(5);
if (!isLock) {
//获取锁失败
return Result.fail("一人只能下一单");
}
try {//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
lock.unlock();
}
锁:
- KEY:标识要锁的资源
- VALUE:标识锁的持有者(用于防误删)
4.4Redis分布式锁误删问题

此时还存在一个问题:线程1超时,锁释放,线程2上锁,线程2进行过程中,线程1结束将锁误删,使得线程3也能上锁
解决方法:释放锁时,获取锁标识并判断是否一致
锁标识:UUID+线程id
为啥不单独用线程id:因为线程id递增,集群中可能冲突

修改一下锁的实现:
标识加上UUID前缀
java
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
java
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + ID_PREFIX + name, id + "", timeoutSec, TimeUnit.SECONDS);
修改释放锁的方法
java
@Override
public void unlock() {
String currentThreadFlag = ID_PREFIX + Thread.currentThread().getId();
String redisThreadFlag = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (currentThreadFlag != null && currentThreadFlag.equals(redisThreadFlag)) {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
4.5分布式锁的原子性问题
分布式锁的原子性问题:线程1判断锁成功,释放锁时阻塞,锁超时自动释放,线程2上锁,线程1阻塞结束将锁误删

解决方法:需确保判断和释放锁一起执行不能间隔(用Lua脚本实现)
Lua脚本中的Redis指令出错,会发生回滚以确保原子性
4.6Lua脚本解决多条命令原子性问题
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
Lua是一种编程语言,基本语法可参考Lua 教程 | 菜鸟教程
Redis提供的调用函数:
shell
redis.call('命令名称','key','其它参数',...)


4.7Java调用lua脚本改造分布式锁
基于Lua脚本实现分布式锁的释放锁逻辑
在java项目中使用Lua脚本调用redis:
- 把Lua脚本文件存入resource包
- 声明静态变量DefaultRedisScript<>用于存储Lua脚本
- 使用stringRedisTemplate.execute调用Lua脚本
在idea中编写Lua文件可以下载一个插件:EmmyLua
1.编写Lua文件
lua
-- 比较缓存中的线程标识与当前线程标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 一致,直接删除
return redis.call('del', KEYS[1])
end
-- 不一致,返回0
return 0
2.声明静态变量
java
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);//声明返回值类型
}
3.修改释放锁的方法,调用Lua脚本
java
@Override
public void unlock() {
//调用Lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
}
4.8Redisson功能介绍
基于setnx实现分布式锁存在以下问题:

使用Redisson可以解决
Redisson是一个在Redis基础上实现的java驻内存数据网络。
它不仅提供了一系列的java常用对象,还提供了许多分布式服务,其中就包含了分布式锁的实现
官网:Redisson | Valkey & Redis Java client. Ultimate Real-Time Data Platform
4.9Redisson快速入门
-
引入依赖:
xml<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency> -
配置Redisson客户端:
java@Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.password}") private String password; /** * 创建Redisson配置对象,然后交给IOC管理 * * @return */ @Bean public RedissonClient redissonClient() { // 获取Redisson配置对象 Config config = new Config(); // 添加redis地址,这里添加的是单节点地址,也可以通过 config.userClusterServers()添加集群地址 config.useSingleServer().setAddress("redis://" + this.host + ":" + this.port) .setPassword(this.password); // 获取RedisClient对象,并交给IOC进行管理 return Redisson.create(config); } } -
使用Redisson的分布式锁
javaRLock lock = redissonClient.getLock("lock:order:" + userId); boolean isLock = lock.tryLock();//Redisson不用手动设置有效期
4.10Redisson的可重入锁原理
什么情况下会使用锁的可重入:
- 递归调用
- 方法链调用
- 回调函数或模版方法模式
- 避免死锁的防御性编程

利用hash结构记录线程id和重入次数
4.11Redisson的锁重试和WatchDog机制

锁重试:加锁失败--->订阅--->唤醒线程--->加锁
WatchDog机制:给锁续期,防止锁失效了业务还没结束
可重试:利用信号量和PubSub功能实现等待、唤醒 获取锁失败的重试机制
超时续约 :利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间
缺陷:Redis宕机引起锁失效问题
4.12Redisson的multiLock原理

原理:多个独立的Redis节点,必须在所有节点都获取重入锁才算获取锁成功
缺陷:运维成本高,实现复杂