【黑马点评学习笔记 | 实战篇 】| 5-分布式锁+初步秒杀优化

Bug如山勤为径,代码似海苦作舟。友友们好,这里是苦瓜大王。今天依旧是黑马点评项目实战篇分布式锁模块的学习,由于之前实现的优惠券秒杀无法避免集群下的线程安全问题,所以我们将在这章节利用分布式锁来对代码进行线程安全方面的进一步优化。
温馨提示:这章很长较难,并且总是优化再优化,优化个没完,建议多看几遍!

笔记如下,后续会一直更新黑马点评学习过程中的笔记、问题等,请多多支持哦!

一、基本原理和不同实现方式对比

1.什么是分布式锁

2.不同实现方案

二、基于Redis的分布式锁

1.实现思路

  • 非阻塞式
shell 复制代码
SET lock thread1 EX 10 NX
DEL lock

2.初级实现

  • 现在不用synchronized来实现了
  • 原代码:
java 复制代码
        synchronized(userId.toString().intern()){  
            //获取代理对象  
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();  
            return proxy.createVoucherOrder(voucherId);  
        }  
  • 修改后代码:
java 复制代码
----------------------------------------lock-----------------------------------------------------
public interface lock {  
    /**  
     * 尝试获取锁  
     * @param timeoutSec 锁的过期时间,单位秒  
     * @return true代表获取锁成功,false代表获取锁失败  
     */  
    boolean tryLock(long timeoutSec);  
    /**  
     * 释放锁  
     * @return  
     */  
    void unLock();  
}
-------------------------------------SimpleRedisLock---------------------------------------------
public class SimpleRedisLock implements  lock{  
    private String name;  
    private StringRedisTemplate stringRedisTemplate;  
    private final static String KEY_PREFIX = "lock:";  
  
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {  
        this.name = name;  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
    @Override  
    public boolean tryLock(long timeoutSec) {  
        //获取线程标识  
        long threadId = 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);  
    }  
}
--------------------------------VoucherOrderServiceImpl----------------------------------
@Resource  
private StringRedisTemplate stringRedisTemplate;  
  
public Result seckillVoucher(Long voucherId) {  
    //1. 查询优惠券  
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);  
    //2. 判断是否在秒杀时间内  
    if (voucher.getBeginTime().isAfter(LocalDateTime.now()))  
        return Result.fail("秒杀尚未开始");  
    if (voucher.getEndTime().isBefore(LocalDateTime.now()))  
        return Result.fail("秒杀已经结束");  
    //3.判断库存是否充足  
    if (voucher.getStock() < 1)  
        return Result.fail("库存不足");  
  
    Long userId = UserHolder.getUser().getId();  
    //锁定的范围是用户id  
    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();  
    }  
  
}

验证分布式锁:

  • 用ApiFox连着发送两条请求
bash 复制代码
http://localhost:8080/api/voucher-order/seckill/10
  • 发现一个顺利获取锁,一个获取失败

3.改进Redis分布式锁误删问题

  • 锁的范围是用户id,不是线程
  • 极端情况下,持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:

  • 在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除

(1)业务流程变化


为什么使用UUID?

  • 多个JVM可能出现线程ID冲突,直接使用线程ID来做标识是不够的,还要区分JVM
  • 创建锁的时候生成一个UUID,锁内部每来一个线程,就把线程ID拼接到后面
  • UUID区分不同JVM,线程ID区分不同线程
  • 由此来确保,不同线程标识一定不一样,相同线程标识一定一样

(2)改进SimpleRedisLock代码

  • 增加线程ID的前缀UUID
java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
  • 获取锁的时候,线程标识加上UUID前缀
java 复制代码
String threadId = ID_PREFIX + Thread.currentThread().getId();
  • 释放锁的时候要判断线程标识是否一致
java 复制代码
//获取线程标识  
String threadId = ID_PREFIX + Thread.currentThread().getId();  
//获取锁中的标识  
String redisThreadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);  
//判断标识是否一致  
if(threadId.equals(redisThreadId)) {  
    //释放锁  
    stringRedisTemplate.delete(KEY_PREFIX + name);  
}
  • 修改后SimpleRedisLock代码如下:
java 复制代码
----------------------------------SimpleRedisLock----------------------------------------
public class SimpleRedisLock implements  lock{  
    private String name;  
    private StringRedisTemplate stringRedisTemplate;  
    private final static String KEY_PREFIX = "lock:";  
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";  
  
    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() {  
        //获取线程标识  
        String threadId = ID_PREFIX + Thread.currentThread().getId();  
        //获取锁中的标识  
        String redisThreadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);  
        //判断标识是否一致  
        if(threadId.equals(redisThreadId)) {  
            //释放锁  
            stringRedisTemplate.delete(KEY_PREFIX + name);  
        }  
    }  
}
  • 现在在集群环境下测试验证改进后的分布式锁
  • 调试的时候,如何跟进一个方法:F7 ,然后Step Over,就可以进入这个方法一步步走

4.改进Redis分布式锁原子性导致的误删问题

  • 由于判断锁标识和释放是两个动作,所以有可能会阻塞(垃圾回收的时候会阻塞该JVM的所有动作)
  • 这两个动作之前产生了阻塞,导致了误删锁
  • 必须保证判断锁和删除锁是原子性的动作,不能出现间隔

(1)Redis的Lua脚本

Lua教程见下面网址:
Lua 教程 | 菜鸟教程

  • 可以传递参数的Lua脚本
  • Lua脚本的执行
  • 直接在redis的命令行就可以执行

(2)编写Lua脚本

  • Lua脚本如下:
Lua 复制代码
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

(3)Java调用Lua脚本改进分布式锁

原代码如下:

java 复制代码
public void unLock() {  
    //获取线程标识  
    String threadId = ID_PREFIX + Thread.currentThread().getId();  
    //获取锁中的标识  
    String redisThreadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);  
    //判断标识是否一致  
    if(threadId.equals(redisThreadId)) {  
        //释放锁  
        stringRedisTemplate.delete(KEY_PREFIX + name);  
    }  
}
1~新建脚本
  • 为了方便以后调整修改脚本,所以不会在unlock方法里写死
  • 下载EmmyLua插件,重启idea
  • 在resources目录下新建一个unlock.lua脚本
  • 在脚本里写入前面的Lua脚本内容
2~调用Lua脚本
  • 在静态代码块里初始化
  • 只会在类加载的时候执行一次,所以不会浪费资源
java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;  
//在静态代码块里初始化  
//只会在类加载的时候执行一次,所以不会浪费资源  
static {  
    UNLOCK_SCRIPT = new DefaultRedisScript<>();  
    //为了避免硬编码,指定lua脚本的位置  
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));  
    //设置返回值类型为Long  
    UNLOCK_SCRIPT.setResultType(Long.class);  
} 
  • 调用Lua脚本:
java 复制代码
@Override  
public void unLock() {  
    //获取线程标识  
    String threadId = ID_PREFIX + Thread.currentThread().getId();  
 //调用Lua脚本  
    stringRedisTemplate.execute(  
            UNLOCK_SCRIPT,  
            //要的是一个集合  
            Collections.singletonList(KEY_PREFIX + name),  
            Collections.singletonList(threadId)  
    );  
}
  • 修改后SimpleRedisLock代码:
java 复制代码
--------------------------------SimpleRedisLock------------------------------------------
public class SimpleRedisLock implements  lock{  
    private String name;  
    private StringRedisTemplate stringRedisTemplate;  
    private final static String KEY_PREFIX = "lock:";  
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";  
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;  
    //在静态代码块里初始化  
    //只会在类加载的时候执行一次,所以不会浪费资源  
    static {  
        UNLOCK_SCRIPT = new DefaultRedisScript<>();  
        //为了避免硬编码,指定lua脚本的位置  
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));  
        //设置返回值类型为Long  
        UNLOCK_SCRIPT.setResultType(Long.class);  
    }  
  
    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() {  
        //获取线程标识  
        String threadId = ID_PREFIX + Thread.currentThread().getId();  
     //调用Lua脚本  
        stringRedisTemplate.execute(  
                UNLOCK_SCRIPT,  
                //要的是一个集合  
                Collections.singletonList(KEY_PREFIX + name),  
                threadId 
        );  
    }  
}

5.总结

三、基于Redis的分布式锁优化------Redisson

  • 由于下面列举的都是一些极端情况,所以可实现可不实现
  • 大多数情况下Lua脚本就够用了,但是面试可能还是会问的
  • 如果对锁的要求很高,就必须去解决这些问题了
  • redisson是在Redis基础上实现的一个分布式工具的集合
  • 具体帮助文档可见GitHub:GitHub_redisson/redisson

1.Redisson优化秒杀*

(1)引入依赖*

java 复制代码
<!--Redisson-->
<dependency>  
        <groupId>org.redisson</groupId>  
    <artifactId>redisson</artifactId>  
    <version>3.13.2</version>  
</dependency>

(2)配置Redisson客户端*

  • 在Config包下新建一个RedisConfig类
java 复制代码
-----------------------------------------RedisConfig-------------------------------------
@Configuration  
public class RedisConfig {  
    @Bean  
    public RedissonClient redissonClient(){  
        //配置  
        Config config = new Config();  config.useSingleServer()
        .setAddress("redis://192.168.255.100:6379")
        .setPassword("468752");  
        
        //创建RedissonClient对象  
        return  Redisson.create(config);  
    }  
}

(3)使用Redisson分布式锁优化秒杀*

  • 注入Redisson
java 复制代码
//注入redisson  
@Resource  
private RedissonClient redissonClient;
  • 改成redisson
java 复制代码
//SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
RLock lock = redissonClient.getLock("lock:order:" + userId);
//获取锁  
boolean isLock = lock.tryLock();//无参,失败不等待,直接返回失败
  • 修改后VoucherOrderServiceImpl代码如下:
java 复制代码
@Service  
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {  
    /**  
     * 秒杀优惠券  
     * @param voucherId 优惠券id  
     * @return 订单id  
     */    @Resource  
    private ISeckillVoucherService seckillVoucherService;  
    @Resource  
    private RedisIdWorker redisIdWorker;  
    @Resource  
    private StringRedisTemplate stringRedisTemplate;  
    //注入redisson  
    @Resource  
    private RedissonClient redissonClient;  
  
    public Result seckillVoucher(Long voucherId) {  
        //1. 查询优惠券  
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);  
        //2. 判断是否在秒杀时间内  
        if (voucher.getBeginTime().isAfter(LocalDateTime.now()))  
            return Result.fail("秒杀尚未开始");  
        if (voucher.getEndTime().isBefore(LocalDateTime.now()))  
            return Result.fail("秒杀已经结束");  
        //3.判断库存是否充足  
        if (voucher.getStock() < 1)  
            return Result.fail("库存不足");  
  
        Long userId = UserHolder.getUser().getId();  
        //锁定的范围是用户id  
        //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();  
        }  
  
    }  
  
    @Transactional  
    public Result createVoucherOrder(Long voucherId) {  
        Long userId = UserHolder.getUser().getId();  
        //4.一人一单  
        //4.1 查询订单  
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();  
        //4.2 判断是否存在  
        if(count>0)  
            return Result.fail("您已经购买过一次了");  
        //5. 扣减库存  
        boolean success = seckillVoucherService.update()  
                .setSql("stock= stock -1")  
                .eq("voucher_id", voucherId)  
                .gt("stock",0)  
                .update(); //where id = ? and stock > 0  
        if(!success)  
            //扣减失败  
            return Result.fail("库存不足");  
  
        //6. 创建订单  
        VoucherOrder voucherOrder = new VoucherOrder();  
        //6.1 订单id  
        long orderId = redisIdWorker.nextId("voucher_order");  
        voucherOrder.setId(orderId);  
        //6.2 用户id  
        voucherOrder.setUserId(userId);  
        //6.3 代金券id  
        voucherOrder.setVoucherId(voucherId);  
  
        //7.订单写入数据库  
        save(voucherOrder);  
  
        //8.返回订单id  
        return Result.ok(orderId);  
    }  
  
}
  • 利用jemter做并发测试,200个线程请求秒杀优惠券

2.Redisson可重入锁原理*

  • 建议多看几遍
    实战篇-19.分布式锁-Redisson的可重入锁原理_哔哩哔哩_bilibili
  • 这种业务流程需要修改,利用哈希结构存储线程标识和重入次数
  • 获取锁的时候不仅要看是否有线程获取了锁,还要看这个线程是不是我自己
  • 当Value变成0的时候,就说明已经到最外层的方法了,没有其他业务需要执行,可以删除锁了
  • 这样复杂的逻辑我们不用java而是Lua脚本去实现,下面简单看一下Lua脚本该怎么写:

(1)获取锁的Lua脚本

(2)释放锁的Lua脚本

  • 追踪源码

  • 获取锁

  • 释放锁

  • 这里有一个发布消息的操作,告诉别人自己释放了锁

3.Redisson的锁重试、WatchDog原理*

4.Redisson的multiLock机制*

  • 解决方案
  • 多个独立节点形成连锁
  • 每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

MutiLock 加锁原理是什么?

  • 当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

四、初步秒杀优化------异步秒杀*

  • 由于数据库的并发性能较差,又是串行执行,所以业务耗时高
  • 优化基本思路:变同步下单为异步下单
  • 优化后的业务流程

1.基于Redis完成秒杀资格判断

(1)业务流程

  • 首先要将库存信息和有关的订单信息保存到redis里
  • 用string和set类型来存储
  • 为了保证原子性,使用Lua脚本来实现
  • 这样耗时短,性能好
  • 再开启一个独立的线程来执行下单和减库存的逻辑

(2)具体代码实现*

1~新增秒杀券时,将优惠券信息存入redis
  • 对VoucherServiceImpl的addSeckillVoucher方法增加将秒杀信息存入Redis的代码
java 复制代码
//先注入
@Resource  
private StringRedisTemplate stringRedisTemplate;

//将秒杀信息保存到redis中  
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
  • 再添加一个秒杀优惠券,查看redis中是否有优惠券id和优惠券库存
2~编写Lua脚本,判断库存、一人一单


  • 完整的Lua脚本如下(增加了阻塞队列版):
lua 复制代码
------------------------------在resources下新建seckill脚本--------------------------------
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
//-- 1.3.订单id
//local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
//-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
//redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
3~ 如果抢购成功将优惠券id和用户id封装后存入阻塞队列
  • 改造VoucherOrderServiceImpl
  • 初始化加载脚本,和之前的逻辑类似
java 复制代码
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;  
    //在静态代码块里初始化  
//只会在类加载的时候执行一次,所以不会浪费资源  
    static {  
        SECKILL_SCRIPT = new DefaultRedisScript<>();  
        //为了避免硬编码,指定lua脚本的位置  
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));  
        //设置返回值类型为Long  
        SECKILL_SCRIPT.setResultType(Long.class);  
    }
  • 执行脚本
java 复制代码
//1.执行Lua脚本  
Long userId = UserHolder.getUser().getId();  
Long result = stringRedisTemplate.execute(  
        SECKILL_SCRIPT,  
        Collections.emptyList(),//脚本的key参数为空,所以传一个空集合  
        voucherId.toString(),//以字符串形式传,具体看execute的参数列表  
        userId.toString()  
);  
//2.判断结果是否为零  
if(result.intValue() != 0)  
    //2.1不为零,没有购买资格  
    return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");  
 //2.2为零,有购买资格,把下单信息保存到阻塞队列中  
  
// TODO 保存到阻塞队列中
  • 封装后存入阻塞队列
java 复制代码
//创建阻塞队列  
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);

    //创建阻塞队列  
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);  
4~开启线程任务,不断从阻塞队列中获取订单,实现异步下单
java 复制代码
//创建线程池  
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();  
  
    //当前类初始化完毕之后就来执行  
    @PostConstruct  
    private void init() {  
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());  
    }  
  
    //创建一个runnable  
    private class VoucherOrderHandler implements Runnable{  
        @Override  
        public void run() {  
            while(true){  
                try {  
                    //1.获取订单中的队列信息  
                    VoucherOrder voucherOrder = orderTasks.take();  
                    //2.创建订单  
                    handleVoucheOrder(voucherOrder);  
                } catch (Exception e) {  
                    log.info("处理订单异常", e);  
                }  
            }  
        }  
    }
 //成员变量,方便子线程获取  
private IVoucherOrderService proxy;
  • 初步优化后的异步秒杀VoucherOrderServiceImpl完整代码:
java 复制代码
@Slf4j  
@Service  
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {  
    /**  
     * 秒杀优惠券  
     * @param voucherId 优惠券id  
     * @return 订单id  
     */    @Resource  
    private ISeckillVoucherService seckillVoucherService;  
    @Resource  
    private RedisIdWorker redisIdWorker;  
    @Resource  
    private StringRedisTemplate stringRedisTemplate;  
    //注入redisson  
    @Resource  
    private RedissonClient redissonClient;  
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;  
    //在静态代码块里初始化  
//只会在类加载的时候执行一次,所以不会浪费资源  
    static {  
        SECKILL_SCRIPT = new DefaultRedisScript<>();  
        //为了避免硬编码,指定lua脚本的位置  
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));  
        //设置返回值类型为Long  
        SECKILL_SCRIPT.setResultType(Long.class);  
    }  
    //创建阻塞队列  
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);  
    //创建线程池  
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();  
  
    //当前类初始化完毕之后就来执行  
    @PostConstruct  
    private void init() {  
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());  
    }  
  
    //创建一个runnable  
    private class VoucherOrderHandler implements Runnable{  
        @Override  
        public void run() {  
            while(true){  
                try {  
                    //1.获取订单中的队列信息  
                    VoucherOrder voucherOrder = orderTasks.take();  
                    //2.创建订单  
                    handleVoucheOrder(voucherOrder);  
                } catch (Exception e) {  
                    log.info("处理订单异常", e);  
                }  
            }  
        }  
    }  
  
    private void handleVoucheOrder(VoucherOrder voucherOrder) {  
        Long userId =voucherOrder.getId();  
        //锁定的范围是用户id  
        RLock lock = redissonClient.getLock("lock:order:" + userId);  
        //获取锁  
        //这一段只是兜底,其实不做也没问题  
        boolean isLock = lock.tryLock();  
        //判断是否获取成功  
        if (!isLock) {  
            //获取锁失败,输出错误  
            log.info("不允许重复下单");  
        }  
        try {  
            //拿到那个现成的代理对象  
            proxy.createVoucherOrder(voucherOrder);  
        } finally {  
            lock.unlock();  
        }  
    }  
    //成员变量,方便子线程获取  
   private IVoucherOrderService proxy;  
    public Result seckillVoucher(Long voucherId) {  
        //1.执行Lua脚本  
        Long userId = UserHolder.getUser().getId();  
        Long result = stringRedisTemplate.execute(  
                SECKILL_SCRIPT,  
                Collections.emptyList(),//脚本的key参数为空,所以传一个空集合  
                voucherId.toString(),//以字符串形式传,具体看execute的参数列表  
                userId.toString()  
        );  
        //2.判断结果是否为零  
        if(result.intValue() != 0)  
            //2.1不为零,没有购买资格  
            return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");  
         //2.2为零,有购买资格,把下单信息保存到阻塞队列中  
  
        //3.保存到阻塞队列中  
        //3.1创建订单  
        VoucherOrder voucherOrder = new VoucherOrder();  
        //3.2订单id  
        long orderId = redisIdWorker.nextId("voucher_order");  
        voucherOrder.setId(orderId);  
        //3.3用户id  
        voucherOrder.setUserId(userId);  
        //3.4优惠券id  
        voucherOrder.setVoucherId(voucherId);  
        //3.5放入阻塞队列  
        orderTasks.add(voucherOrder);  
  
        //4.获取代理对象(因为子线程无法获取父线程的代理)  
        //获取代理对象(初始化)  
        proxy = (IVoucherOrderService) AopContext.currentProxy();  
        // 4. 返回订单id  
        return Result.ok(orderId);  
    }  
  
    //<--异步秒杀之前的代码-->  
    /*public Result seckillVoucher(Long voucherId) {        //1. 查询优惠券  
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);        //2. 判断是否在秒杀时间内  
        if (voucher.getBeginTime().isAfter(LocalDateTime.now()))            return Result.fail("秒杀尚未开始");  
        if (voucher.getEndTime().isBefore(LocalDateTime.now()))            return Result.fail("秒杀已经结束");  
        //3.判断库存是否充足  
        if (voucher.getStock() < 1)            return Result.fail("库存不足");  
  
        Long userId = UserHolder.getUser().getId();        //锁定的范围是用户id  
        //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();        }  
    }*/  
  
    @Transactional  
    public void createVoucherOrder(VoucherOrder voucherOrder) {  
        //异步的,所以不能通过ThreadLocal来获取了  
        Long userId =voucherOrder.getId();  
        //4.一人一单  
        //4.1 查询订单  
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();  
        //4.2 判断是否存在  
        if(count>0)  
            log.info("您已经购买过一次了");  
        //5. 扣减库存  
        boolean success = seckillVoucherService.update()  
                .setSql("stock= stock -1")  
                .eq("voucher_id", voucherOrder.getVoucherId())  
                .gt("stock",0)  
                .update(); //where id = ? and stock > 0  
        if(!success)  
            //扣减失败  
            log.info("库存不足");  
        //6.订单写入数据库  
        save(voucherOrder);  
    }  
}
  • 这里只是基于redis的异步秒杀的初步优化,后续将继续结合Redis消息队列,解决内存限制、数据安全问题,具体见下一篇笔记
相关推荐
金蕊泛流霞2 小时前
Spring AI Alibaba笔记
java·笔记·spring
艾莉丝努力练剑2 小时前
System V IPC底层原理详解
linux·运维·服务器·网络·c++·人工智能·学习
PNP Robotics2 小时前
PNP机器人分享Frankal机器人等具身案例开发和实践
大数据·python·学习·机器人·开源
Dxy12393102162 小时前
HTML中图表学习:从基础到实战指南
前端·学习·html
电子云与长程纠缠2 小时前
Godot学习01 - HelloWorld
学习·游戏引擎·godot
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2025.03.18 题目:3070.元素和小于等于k的子矩阵的数目
笔记·leetcode·矩阵
Nan_Shu_6142 小时前
学习:Cesium (2)
学习
电子云与长程纠缠2 小时前
Godot学习02 - 输入
java·学习·godot
啊阿狸不会拉杆2 小时前
《计算机网络-自顶向下方法》笔记分享:第1章-「计算机网络和因特网」-1.2 网络边缘
网络·笔记·计算机网络·接入网·光纤·网络边缘·物理媒体