目录
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
Redis实现全局唯一Id- @Component
- public class RedisIdWorker {
- /**
- * 开始时间戳
- */
- private static final long BEGIN_TIMESTAMP = 1640995200L;
- /**
- * 序列号的位数
- */
- private static final int COUNT_BITS = 32;
-
- private StringRedisTemplate stringRedisTemplate;
-
- public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
- this.stringRedisTemplate = stringRedisTemplate;
- }
-
- public long nextId(String keyPrefix) {
- // 1.生成时间戳
- LocalDateTime now = LocalDateTime.now();
- long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
- long timestamp = nowSecond - BEGIN_TIMESTAMP;
-
- // 2.生成序列号
- // 2.1.获取当前日期,精确到天
- String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
- // 2.2.自增长
- long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
-
- // 3.拼接并返回
- return timestamp << COUNT_BITS | count;
- }
- }
countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题
我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch
CountDownLatch 中有两个最重要的方法
await 方法 是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。
- @Test
- void testIdWorker() throws InterruptedException {
- CountDownLatch latch = new CountDownLatch(300);
-
- Runnable task = () -> {
- for (int i = 0; i < 100; i++) {
- long id = redisIdWorker.nextId("order");
- System.out.println("id = " + id);
- }
- latch.countDown();
- };
- long begin = System.currentTimeMillis();
- for (int i = 0; i < 300; i++) {
- es.submit(task);
- //这个循环将任务task提交到一个ExecutorService(通常是线程池)中执行。循环执行300次,
- //因此会创建300个线程来生成订单ID。
- }
- latch.await();
- long end = System.currentTimeMillis();
- System.out.println("time = " + (end - begin));
- }
- /*
- 这段代码通过并发执行多个线程,每个线程生成一系列订单ID,然后等待所有线程完成。
- 最后,它计算并打印出总体测试的执行时间,以评估生成订单ID的性能。此测试用例旨在
- 测试redisIdWorker的性能,特别是在并发情况下生成订单ID的效率。测试结果可以用来
- 评估系统在高负载情况下的性能表现。
- */


秒杀下单应该思考的内容:
下单时需要判断两点:
下单核心逻辑分析:
当用户开始进行下单,我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件,比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

VoucherOrderServiceImpl
- @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("库存不足!");
- }
- //5,扣减库存
- boolean success = seckillVoucherService.update()
- .setSql("stock= stock -1")
- .eq("voucher_id", voucherId).update();
- if (!success) {
- //扣减库存
- return Result.fail("库存不足!");
- }
- //6.创建订单
- VoucherOrder voucherOrder = new VoucherOrder();
- // 6.1.订单id
- long orderId = redisIdWorker.nextId("order");
- voucherOrder.setId(orderId);
- // 6.2.用户id
- Long userId = UserHolder.getUser().getId();
- voucherOrder.setUserId(userId);
- // 6.3.代金券id
- voucherOrder.setVoucherId(voucherId);
- save(voucherOrder);
-
- return Result.ok(orderId);
-
- }
假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。


悲观锁:
悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等
乐观锁:
乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

初步代码:增加一人一单逻辑
- @Transactional
- public synchronized Result createVoucherOrder(Long voucherId) {
-
- 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") // set stock = stock - 1
- .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and 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);
-
- // 7.返回订单id
- return Result.ok(orderId);
- }
但是这样添加锁,锁的粒度太粗了,在使用锁过程中,控制锁粒度 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度,以下这段代码需要修改为: intern() 这个方法是从常量池中拿到数据,如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象,new出来的对象,我们使用锁必须保证锁必须是同一把,所以我们需要使用intern()方法
- @Transactional
- public Result createVoucherOrder(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- synchronized(userId.toString().intern()){
- // 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") // set stock = stock - 1
- .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and 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);
-
- // 7.返回订单id
- return Result.ok(orderId);
- }
- }
但是以上代码还是存在问题,问题的原因在于当前方法被spring的事务控制,如果你在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放也会导致问题,所以我们选择将当前方法整体包裹起来,确保事务不会出现问题:如下:
在seckillVoucher 方法中,添加以下逻辑,这样就能保证事务的特性,同时也控制了锁的粒度
(在调用处上锁)

但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路



Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案,由于本套视频并不讲解zookeeper的原理和分布式锁的实现,所以不过多阐述

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

加锁逻辑
锁的基本接口

SimpleRedisLock
利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
- private String name;//业务名称
- 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);
- //setIfAbsent 因为需要nx ex操作 nx是互斥,ex是超时
- //KEY_PREFIX + name是key
- //threadId 是value
- return Boolean.TRUE.equals(success);
- }
释放锁逻辑
SimpleRedisLock
- public void unlock() {
- //通过del删除锁
- stringRedisTemplate.delete(KEY_PREFIX + name);
- }
业务代码
- @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(1200);
- //加锁失败
- if (!isLock) {
- return Result.fail("不允许重复下单");
- }
- try {
- //获取代理对象(事务)
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- } finally {
- //释放锁
- lock.unlock();
- }
- }
逻辑说明:
持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明
解决方案:
解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

需求:修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用UUID表示)
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
- 如果一致则释放锁
- 如果不一致则不释放锁
核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
加锁
- 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);
- return Boolean.TRUE.equals(success);
- }
释放锁
- 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进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,
Lua脚本解决多条命令原子性问题Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程,这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了,作为Java程序员这一块并不作一个简单要求,并不需要大家过于精通,只需要知道他有什么作用即可。
这里重点介绍Redis提供的调用函数,语法如下:
redis.call('命令名称', 'key', '其它参数', ...)例如,我们要执行set name jack,则脚本是这样:
# 执行 set name jack redis.call('set', 'name', 'jack')例如,我们要先执行set name Rose,再执行get name,则脚本如下:
# 先执行 set name jack redis.call('set', 'name', 'Rose') # 再执行 get name local name = redis.call('get', 'name') # 返回 return name写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
例如,我们要执行 redis.call('set', 'name', 'jack') 这个脚本,语法如下:
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示 -- 获取锁中的标示,判断是否与当前线程标示一致 if (redis.call('GET', KEYS[1]) == ARGV[1]) then -- 一致,则删除锁 return redis.call('DEL', KEYS[1]) end -- 不一致,则直接返回 return 0
lua脚本本身并不需要大家花费太多时间去研究,只需要知道如何调用,大致是什么意思即可,所以在笔记中并不会详细的去解释这些lua表达式的含义。
我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图股

- unlock.lua 文件内容为:
-
- if (redis.call('GET', KEYS[1]) == ARGV[1]) then
- -- 一致,则删除锁
- return redis.call('DEL', KEYS[1])
- end
- -- 不一致,则直接返回
- return 0
-
-
- //提前引入脚本,避免后续频繁的IO流
- //它是一个DefaultRedisScript类型的对象。这个对象用于执行Redis Lua脚本,并且返回一个Long类型的结果。
- private static final DefaultRedisScript
UNLOCK_SCRIPT; - static {
- UNLOCK_SCRIPT = new DefaultRedisScript<>();
- UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
- //setLocation方法设置Lua脚本文件的位置。这里假设Lua脚本文件名为"unlock.lua",并且该文件位
- //于类路径(Classpath)下。这个Lua脚本通常包含了解锁操作的逻辑。
- UNLOCK_SCRIPT.setResultType(Long.class);
- //这是因为在分布式锁的情况下,通常会返回一个整数值,以表示解锁是否成功或解锁的条件。
- }
-
- public void unlock() {
- // 调用lua脚本
- stringRedisTemplate.execute(
- UNLOCK_SCRIPT,
- Collections.singletonList(KEY_PREFIX + name),
- ID_PREFIX + Thread.currentThread().getId());
- }
基于Redis的分布式锁实现思路:
利用set nx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁
笔者总结:我们一路走来,利用添加过期时间,防止死锁问题的发生,但是有了过期时间之后,可能出现误删别人锁的问题,这个问题我们开始是利用删之前 通过拿锁,比锁,删锁这个逻辑来解决的,也就是删之前判断一下当前这把锁是否是属于自己的,但是现在还有原子性问题,也就是我们没法保证拿锁比锁删锁是一个原子性的动作,最后通过lua表达式来解决这个问题
但是目前还剩下一个问题锁不住,什么是锁不住呢,你想一想,如果当过期时间到了之后,我们可以给他续期一下,比如续个30s,就好像是网吧上网, 网费到了之后,然后说,来,网管,再给我来10块的,是不是后边的问题都不会发生了,那么续期问题怎么解决呢,可以依赖于我们接下来要学习redission
基于setnx实现的分布式锁存在下面的问题:
重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。
超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患
主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
Redission提供了分布式锁的多种多样的功能

引入依赖:
-
org.redisson -
redisson -
3.13.6
配置Redisson客户端:
- @Configuration
- public class RedissonConfig {
-
- @Bean
- public RedissonClient redissonClient(){
- // 配置
- Config config = new Config();
- config.useSingleServer().setAddress("redis://192.168.150.101:6379")
- .setPassword("123321");
- // 创建RedissonClient对象
- return Redisson.create(config);
- }
- }
如何使用Redission的分布式锁
- @Resource
- private RedissionClient redissonClient;
-
- @Test
- void testRedisson() throws Exception{
- //获取锁(可重入),指定锁的名称
- RLock lock = redissonClient.getLock("anyLock");
- //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
- boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
- //判断获取锁成功
- if(isLock){
- try{
- System.out.println("执行业务");
- }finally{
- //释放锁
- lock.unlock();
- }
- }
- }
在 VoucherOrderServiceImpl
注入RedissonClient
- @Resource
- private RedissonClient redissonClient;
-
- @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);
- RLock lock = redissonClient.getLock("lock:order:" + userId);
- //获取锁对象
- boolean isLock = lock.tryLock();
-
- //加锁失败
- if (!isLock) {
- return Result.fail("不允许重复下单");
- }
- try {
- //获取代理对象(事务)
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- } finally {
- //释放锁
- lock.unlock();
- }
- }
在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state类似,也是重入一次就加一,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。
在redission中,我们的也支持支持可重入锁
在分布式锁中,他采用hash结构用来存储锁,

(当前图 代表锁已释放 由于value=0 那么到了最外层的方法 那么就可以释放锁)
其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有,所以接下来我们一起分析一下当前的这个lua表达式
这个地方一共有3个参数
KEYS[1] : 锁名称
ARGV[1]: 锁失效时间
ARGV[2]: id + ":" + threadId; 锁的小key
exists: 判断数据是否存在 name:是lock是否存在,如果==0,就表示当前这把锁不存在
redis.call('hset', KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构
Lock{
id + ":" + threadId : 1
}
如果当前这把锁存在,则第一个条件不满足,再判断
redis.call('hexists', KEYS[1], ARGV[2]) == 1
此时需要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则进行
redis.call('hincrby', KEYS[1], ARGV[2], 1)
将当前这个锁的value进行+1 ,redis.call('pexpire', KEYS[1], ARGV[1]); 然后再对其设置过期时间,如果以上两个条件都不满足,则表示当前这把锁抢锁失败,最后返回pttl,即为当前这把锁的失效时间
他会去判断当前这个方法的返回值是否为null,如果是null,则对应则前两个if对应的条件,退出抢锁逻辑,如果返回的不是null,即走了第三个分支,在源码处会进行while(true)的自旋抢锁。

- ...
- long threadId = Thread.currentThread().getId();
- Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
- // lock acquired
- if (ttl == null) {
- return;
- }
- ...
抢锁过程中,获得当前线程,通过tryAcquire进行抢锁,该抢锁逻辑和之前逻辑相同
1、先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null
2、判断当前这把锁是否是属于当前线程,如果是,则返回null
所以如果返回是null,则代表着当前这哥们已经抢锁完毕,或者可重入完毕,但是如果以上两个条件都不满足,则进入到第三个条件,返回的是锁的失效时间,同学们可以自行往下翻一点点,你能发现有个while( true) 再次进行tryAcquire进行抢锁