LongZ

积硅步,至千里

  • 主页
  • 随笔
  • 杂记
  • 算法
  • 归档
所有文章 关于我

LongZ

积硅步,至千里

  • 主页
  • 随笔
  • 杂记
  • 算法
  • 归档

Redis分布式锁

2023-08-27
字数统计: 1.4k字   |   阅读时长: 5分

阅读数: 次   

分布式锁的核心就是实现多线程之间的互斥,能够实现这一点的方式有很多,比如MySQL的的锁机制,而今天要说的是Redis。在上一篇文章中提到,在单体服务器下,前面的代码逻辑是可以很好地实现的,但是在集群的时候,由于每台服务器都有自己的锁监视器,所以不能保证每个用户获取到的锁是同一把锁,也就无法保证一人一单的问题。

所以今天要说的分布式锁就是为了解决在集群模式下,如何保证锁在多线程的情况下,每个用户拿到的锁是同一把锁。

实现分布式锁需要两个方法:

  • 获取锁:

    互斥:确保只能有一个线程获取锁

    非阻塞:尝试一次,成功返回true,失败返回false

  • 释放锁:

    手动释放

    超时释放,获取锁的时候添加一个超时时间

这里是基于UUID + 锁ID来标记锁的 key,保证锁的唯一性的。下面是实现步骤:

1、首先定义一个接口,定义好获取锁和释放锁的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface ILock {
/**
* 尝试获取锁,非阻塞性
* @param timeoutSec 设置锁持有的超时时间,过期后自动释放
* @return 返回值为true为获取成功,返回false则获取锁失败
*/
boolean tryLock(Long timeoutSec);

/**
* 主动释放锁
*/
void unlock();
}

2、定义一个类,实现上面的接口,利用Redis实现分布式锁功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SimpleRedisLock implements ILock {
@Autowired
private StringRedisTemplate stringRedisTemplate;

private String name; // 业务名称(传入参数)
private static final String LOCK_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}

@Override
public boolean tryLock(Long timeoutSec) {
// 获取当前虚拟机的锁名称,用UUID去拼接成一个独一无二的锁标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 判断获取锁是否成功
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(LOCK_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 自动拆包有风险,可能为 null (下面是两种判断方法)
//return Boolean.TRUE.equals(success);
return BooleanUtil.isTrue(success);
}

@Override
public void unlock() {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取到Redis服务器的锁的值
String id = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);
// 判断这个锁的值与自己的值是否一致,一致才能自己释放
if (threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(LOCK_PREFIX + name);
}
}
}

这里获取分布式锁的逻辑很简单,就是使用多种验证,先用UUID + 当前虚拟机的线程id来作为标识,保证唯一性;在释放锁的时候,也需要判断是否是同一个线程才允许释放锁,如果不加以判断的话,若当前线程在执行业务过程中,出现卡顿异常情况而导致的锁超时释放,这时其他线程会乘机获取锁,等到当前线程恢复的时候,就会把其他线程的锁释放掉而造成更大的问题。

笔者这里多次使用到了hutool包,这是一个很好用的工具包,在允许使用的情况下,可以去官网查看。

3、改造秒杀下单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public Result seckillVoucher(Long voucherId) {
// 1. 根据id查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
// 2.1 秒杀是否已经开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始!");
}
// 2.2 秒杀是否已经结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束!");
}
// 3. 判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足!");
}
// 业务的名称,拼接锁的时候使用
String name = "voucher-order:" + userId;
SimpleRedisLock redisLock = new SimpleRedisLock(stringRedisTemplate, name);
boolean isLock = redisLock.tryLock(10L);
// 判断是否获取分布式锁成功
if (!isLock) {
// 获取失败
return Result.fail("服务器繁忙,请稍后重试!");
}
// 获取锁成功
try {
// 获取代理对象(事务)
IVoucherOrderService proxy =
(IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(userId, voucherId);
} finally {
// 释放锁
redisLock.unlock();
}
}

createVoucherOrder方法的逻辑不变,只是获取锁的时候,把悲观锁换成了分布式锁,保证在集群下也能够实现一人一单的业务需求。

以上就是基于Redis实现分布锁来解决集群模式下一人一单问题的解决方法,但正如前面所说,如果当前线程执行业务过程中,遇到JVM正在GC的情况,就会造成业务阻塞,等到线程恢复的时候,可能锁已经超时失效了,即便在释放锁放在 finally关键字里面,也是无法保证命令的原子性的。比如,在线程获取到锁,刚刚判断完获取锁成功的时候,就出现了卡顿,造成业务阻塞,等到线程恢复的时候,此时锁已经超时释放了,这里的解决策略可以是延长锁超时时间,但在高并发情况下,可能不太适用。这里推荐Lua脚本,这是一个轻量级的工具,一个脚本可以编写多条Redis命令,因为命令都是在同一个脚本里面的,可以很好地保证它的原子性。

下一篇文章会继续优化Redis分布式锁的原子性问题。

本文作者: Long HY
本文链接: https://longzas.github.io/2023/08/27/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!
  • Redis高并发
  • Redis
Lua脚本优化分布式锁原子性问题
Redis高并发场景:商品秒杀活动

Gitalking ...

© 2023 Long HY
总访问量:次 | 总访客:人
  • 所有文章
  • 关于我

tag:

  • 杂记
  • 随笔
  • Redis高并发
  • Redis缓存
  • Java多线程
  • 算法
今夜无事,勾栏听曲

积硅步,至千里。

     共同努力!

邮箱地址:longhy1008@qq.com