黑马点评学习笔记10(优惠券秒杀下单优化(分布式锁的优化,Lua脚本))

前言

前面讨论了Redis实现优惠券秒杀系统中的线程安全问题,用悲观锁和乐观锁解决了多个线程同时查询并修改库存导致负数库存。代码示例展示了: 乐观锁实现库存扣减(CAS方式) 用户订单数量检查 订单创建流程 整个系统通过线程安全机制保证了高并发场景下的数据一致性,下面继续讨论线程安全的问题 :

只是前面的sychronized锁住的只是一个用户的访问,每一个JVM都有一个独立的锁监视器,因此我们要用到分布式锁:


分布式锁的常见实现方式:


基于Redis的分布式锁:

1. 分布式锁(SimpleRedisLock)

用于实现"一人一单"的并发控制,防止用户重复下单。

涉及的 Redis 命令:

  • SET key value NX PX timeout
    • 用于尝试获取锁。
    • NX:只有当 key 不存在时才设置(保证互斥)。
    • PX:设置过期时间(毫秒),防止死锁。
      示例(伪命令):
bash 复制代码
SET order:123 "thread-id" NX PX 1200000

setIfAbsent(key, value, timeout) 是 Spring 对 SET key value NX PX 的封装。

返回 true 表示加锁成功,false 表示锁已被占用。

java 复制代码
// 对应 Redis 命令: SET order:123 "lockValue" NX PX 1200
Boolean isLock = stringRedisTemplate.opsForValue()
    .setIfAbsent("order:" + userId, "anyValue", Duration.ofMillis(1200));
  • DEL key(带校验)

1)来看看具体代码吧,创建分布式锁:

我们再新建一个类:

java 复制代码
package com.hmdp.utils;

import ...

public class SimpleRedisLock implements ILock{

    /*
    锁的名称
     */
    private String name;
    /*
    Redis操作客户端
     */
    private StringRedisTemplate stringRedisTemplate;

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


    /*
    尝试获取锁
     */
    @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);
    }

   
    /*
    释放锁
     */
    @Override
    public void unLock() {
          stringRedisTemplate.delete(KEY_PREFIX + name);
       }
    }
}

使用分布式锁:

java 复制代码
package com.hmdp.service.impl;

import ...

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    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("库存不足");
        }

 
        // 3、创建订单(使用分布式锁)
        Long userId = UserHolder.getUser().getId();
        SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
        boolean isLock = lock.tryLock(1200);
        if (!isLock) {
            // 索取锁失败,重试或者直接抛异常(这个业务是一人一单,所以直接返回失败信息)
            return Result.fail("一人只能下一单");
        }
        try {
            // 索取锁成功,创建代理对象,使用代理对象调用第三方事务方法, 防止事务失效
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(userId, voucherId);
        } finally {
            lock.unLock();
        }



    }

    /*
    创建订单
     */
    @Transactional  //添加事务保证数据库操作和缓存操作的原子性
    public Result createVoucherOrder(Long voucherId) {
        //5.一人一单
        //5.1 查询订单
        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)//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.setVoucherId(voucherId);
            //7.3.用户ID
            voucherOrder.setUserId(userId);

            //写入数据库
            save(voucherOrder);

            //6.返回订单ID
            return Result.ok(orderId);

    }



}

分布式锁优化

1.分布式锁可能出现的问题1:


上面实现了一个简单的分布式锁,其实还存在一些问题,就像上面的线程一获取锁后,然后一些原因业务阻塞了,然后锁呢超时释放了,这时候线程二,趁虚而入,获取锁成功后,线程一完成了把锁给释放了,这时县城三又开始获取锁了,这就导致超卖问题了。

来看看怎么解决吧

我们在释放锁时检查一下是不是自己的锁不就行了,不是自己的线程锁就不可以释放,是自己的就可以释放。

来看一下代码怎么实现吧:
java 复制代码
package com.hmdp.utils;

import...

public class SimpleRedisLock implements ILock{

    /*
    锁的名称
     */
    private String name;
    /*
    Redis操作客户端
     */
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    /**
    *key的前缀
    */

    private static final String KEY_PREFIX = "lock:";
   
    /*
    尝试获取锁
     */
    @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);
    }

  
  

    @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);
        }
    }
}
分布式锁可能出现的问题2

分布式锁的原子问题:

当线程一获取锁成功,执行完业务获取锁标识成功后正好要释放锁时,线程阻塞了,这是线程二趁虚而入了,获得锁成功,这是线程一又好了,直接释放锁了(因为之前判断过了),又导致线程三趁虚而入了,又产生超卖问题了。

来看看怎么解决呢?

为了避免上书情况的发生,我们需要保证判断锁释放锁这两个方法的原子性,怎么保证原子性呢?

先来看看什么是Lua脚本:

一、什么是 Lua 脚本?
Lua 是一种轻量级、高效的脚本语言,常被嵌入到其他应用程序中。在 Redis 中,可以通过 EVAL 或 EVALSHA 命令执行 Lua 脚本。

在 Redis 中使用 Lua 脚本?

  • 原子性(Atomicity):
    • Redis 是单线程执行命令的。
    • 当你通过 EVAL 执行一段 Lua 脚本时,整个脚本会在 Redis 服务器端以原子方式执行,期间不会被其他客户端的请求打断。
    • 这对于实现复杂的逻辑(比如检查+删除)非常关键,可以避免竞态条件。
  • 减少网络开销:
    • 多个 Redis 操作可以封装在一个脚本中,只需要一次网络调用即可完成。
  • 可复用性和安全性:
    • 脚本可以预加载或通过 SHA 缓存重复执行。
    • 在服务端执行,避免中间状态暴露给客户端。


1.编写Lua脚本:

在代码里新建一个Lua文件,记得装插件:Tarantool-EmmyLua


在lua脚本中编写要保证原子性的Redis命令

lua 复制代码
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Cecilia.
--- DateTime: 2025/10/24 11:30
---比较线程标识与锁中的标识是否一致
if(redis.call('get',KEY[1]) == ARGV[1]) then
    --释放锁 del key
    return redis.call('del', KEYS[1])
end
-- 不一致,返回0
return 0

编写Java代码,使用Lua改进分布式锁:

java 复制代码
package com.hmdp.utils;

import...

public class SimpleRedisLock implements ILock{

    /*
    锁的名称
     */
    private String name;
    /*
    Redis操作客户端
     */
    private StringRedisTemplate stringRedisTemplate;

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

    /*
    key前缀
    */
    private static final String KEY_PREFIX = "lock:";
    /*
    ID前缀
    */
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";


    /*
    尝试获取锁
     */
    @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);
    }

    /*
    加载Lua脚本
    */
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /*
    释放锁
     */

    public void unLock() {
       //调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
                );

    }

}
java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";

ID_PREFIX:每个 JVM 实例启动时生成一个唯一 UUID,加上线程 ID,构成锁的 value。

🔒 获取锁:tryLock(long timeoutSec)
java 复制代码
@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);
}

对应的 Redis 命令:

bash 复制代码
SET lock:order:123 "a1b2c3-12345" EX 120 NX
✅ 1. Lua 脚本的声明与加载
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);
}

在类加载时,从 classpath 下加载名为 unlock.lua 的 Lua 脚本文件,并封装为 DefaultRedisScript 对象。

✅ 2. Lua 脚本的执行(释放锁)
java 复制代码
public void unLock() {
    stringRedisTemplate.execute(
        UNLOCK_SCRIPT,
        Collections.singletonList(KEY_PREFIX + name),
        ID_PREFIX + Thread.currentThread().getId()
    );
}
  • 调用方式:使用 StringRedisTemplate.execute() 执行预定义的 Lua 脚本。
  • 参数说明:
    • 第一个参数:UNLOCK_SCRIPT ------ 已加载的 Lua 脚本对象
    • 第二个参数:KEYS 列表 → 只传一个 key:lock:xxx
    • 第三个参数:ARGV 值 → 当前线程的唯一标识(如 a1b2c3-12345)
✅ 3. Lua 脚本内容(unlock.lua 文件)

将三个参数传到Lua脚本中执行,Redis命令保证了其执行的原子性。

lua 复制代码
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Cecilia.
--- DateTime: 2025/10/24 11:30
---比较线程标识与锁中的标识是否一致
if(redis.call('get',KEY[1]) == ARGV[1]) then
    --释放锁 del key
    return redis.call('del', KEYS[1])
end
-- 不一致,返回0
return 0

本文是学习黑马程序员---黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)

...。。。。。。。。。。。...

...。。。。。。。。。。。...

相关推荐
递归不收敛2 小时前
config.json 完全指南:项目配置的核心实践
笔记·学习·json
小坏讲微服务2 小时前
使用 Spring Cloud Gateway 实现集群
java·spring boot·分布式·后端·spring cloud·中间件·gateway
shenghaide_jiahu2 小时前
字符串匹配和回文串类题目
学习·算法·动态规划
一个平凡而乐于分享的小比特3 小时前
UCOS-III笔记(一)
笔记·ucosiii
驯狼小羊羔3 小时前
学习随笔-http和https有何区别
前端·javascript·学习·http·https
风无雨3 小时前
gin学习
学习·gin
失散133 小时前
分布式专题——53 ElasticSearch高可用集群架构实战
java·分布式·elasticsearch·架构
lkbhua莱克瓦243 小时前
Java入门——Java跨平台的原理
java·开发语言·笔记·github
皓木.3 小时前
软件测试-app测试
笔记