引入
在jvm内部只有一个锁监视器,所以只有一个线程可以获取锁,可以实现线程间的互斥
但是,当有多个jvm的时候,就会有多个锁监视器,就会有多个线程获取到锁,这样就没有办法实现多jvm进程之间的互斥了
要解决这个问题,就不能再用jvm的锁监视器了,而是在jvm外有一个共同使用的锁监视器
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
基于redis的分布式锁实现思路
实现分布式锁需要实现的两个基本方法:
1.获取锁
-
互斥:确保只能有一个线程获取锁
-
非阻塞:尝试一次,成功返回true,失败返回false
添加锁,NX是互斥,EX是设置超时时间
SET lock thread1 NX EX 10
2.释放锁
-
手动释放
-
超时释放:获取锁时添加一个超时时间
释放锁,删除即可
DEL key
流程图:

代码实现1.0
接口:
java
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁的过期时间,单位秒
* @return true代表获取锁成功,false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unLock();
}
工具类:
java
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = Thread.currentThread().getId() + "";
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
实现类:
java
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始");
}
// 3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 已经结束
return Result.fail("秒杀已经结束");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足");
}
Long userId= UserHolder.getUser().getId();
// 创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 获取锁
boolean isLock = lock.tryLock(1200L);
// 判断是否获取锁成功
if (!isLock) {
// 获取锁失败,返回错误信息
return Result.fail("不允许重复下单");
}
try {
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
}finally {
// 释放锁
lock.unLock();
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 5.一人一单
Long userId = UserHolder.getUser().getId();
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 存在,不能重复下单
return Result.fail("不能重复下单");
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足");
}
// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2 用户id
voucherOrder.setUserId(userId);
// 7.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 8.返回订单id
return Result.ok(orderId);
}
}
存在问题:
假设线程1获取到锁了,但是业务阻塞了,于是锁超时释放了,这时线程2获取到了锁,正在执行自己业务的过程中,线程1的业务完成了,这个时候线程1就会把线程2的锁给释放掉,这时线程3就能获取到锁,导致线程2和线程3同时获取到了锁,出现了安全问题
解决方法:
释放锁时需要获取锁标识并判断是否一致,一致才能释放锁
流程图:

代码实现2.0
工具类
java
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
// 使用hutool包的UUID来唯一标识jvm进程
private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标识是否一致
if (threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
解决了上述问题,但是仍然存在问题:
线程1在获取锁标识并判断是一致的时候,刚要释放锁,突然阻塞了,导致锁超时释放,这时线程2获取到了锁,开始执行自己的业务。。。(接下来与上一个问题一样)
解决思路:把判断和释放做成一个原子性的动作。
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
Lua
Lua是一种编程语言,它的基本语法可以参考网站:Lua 教程 | 菜鸟教程
redis提供的调用函数
语法:
java
# 执行redis命令
redis.call('命令名称', 'key', '其他参数', ...)
例如,我们要执行set name jack,则脚本是这样:
java
# 执行set name jack
redis.call('set', 'name', 'jack')
例如,我们要先执行set name jack,再执行get name,则脚本如下:
java
# 先执行set name jack
redis.call('set', 'name', 'jack')
# 在执行get name
local name = redis.call('get', 'name')
# 返回
return name
调用脚本
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
java
EVAL script numkeys key [key ...] arg [arg ...]
例如,我们要执行redis.call('set','name',jack')这个脚本,语法如下:
java
# 调用脚本
EVAL "return redis.call('set', 'name', 'jack')" 0
双引号内为脚本内容,后面的数字为脚本需要的key类型的参数个数
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
java
# 调用脚本
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
name对应KEYS[1],Rose对应ARGV[1]
在Lua中,第一个元素的下标为1
代码实现3.0
释放锁的Lua脚本
Lua
-- 获取锁中的线程标识
local id = redis.call("GET", KEYS[1])
-- 比较线程标识与锁中的标识是否一致
if (id == ARGV[1]) then
-- 一致则删除锁,释放锁
return redis.call("DEL", KEYS[1])
else
-- 不一致则直接返回0,表示未释放锁
return 0
end
首先安装Lua的插件
然后在resources下新建一个Lua脚本写入这段代码
工具类
java
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
// 声明静态常量:脚本对象是类级别的,全局唯一且不可修改
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// 静态代码块:类加载时初始化这个常量
static {
// 第一步:创建 Redis Lua 脚本对象实例(只 new 一次)
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// 第二步:指定 Lua 脚本文件的路径(加载脚本内容,只加载一次)
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// 第三步:指定脚本执行后的返回值类型(只配置一次)
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
// 调用Lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}
这样就解决了上述问题,但是还可以优化
Redisson框架
Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址: Redisson | Valkey & Redis Java client. Ultimate Real-Time Data Platform
Redisson入门
1.引入依赖
XML
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2.配置Redisson客户端
java
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置类
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://192.168.100.128:6379").setPassowrd("1234") ;
// 创建客户端
return Redisson.create(config);
}
}
3.使用Redisson的分布式锁
java
@Resource
private Redissonclient redissonclient;
@Test
void testRedisson() throws InterruptedException {
//获取锁(可重入),指定锁的名称
RLock lock = redissonclient.getLock("anyLock");
// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SEcoNDS);
// 判断释放获取成功
if(isLock){
try {
System.out.println("执行业务");
}finally {
// 释放锁
Lock.unlock();
}
}
}
Redisson功能
解决了不可重入、不可重试、超时释放、主从一致性的问题
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试 :获取锁只尝试一次就返回false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
- 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现