视频地址
考虑分布式场景下ID的全局唯一性
分库分表后,id主键如何处理
① 数据库自增 id:先从一个数据库表中获取自增id,再根据该 id 往对应的分库分表中写入
问题:生成id的数据库表高并发瓶颈
适用场景:并发不高,数据量太大导致的分库分表
② uuid:UUID.randomUUID()
问题:不适用于实际的业务需求,生成的订单号UUID字符串看不出与订单相关的有用信息。长字符串【存储性能差,查询耗时】
③ 获取系统当前时间:系统时间作为主键
问题:秒级并发时,会有重复情况
适用场景:业务字段与当前时间拼接,组成全局唯一编号
④ redis:redis 的 incr 实现 ID 原子性自增
问题:redis 持久化过程中出现宕机时,RDB持久化会出现重复 id 的情况,AOF 持久化会导致重启恢复数据时间过长 【问题:什么原因导致了两者的差异?】
⑤ 雪花算法:时间戳 + 机器 id + 序列号
问题:强依赖机器时钟
视频中采用 redis 生成全局唯一 id
① 业务名称前缀 + 当前日期 作为 redis 自增长参数 key
//2.2 自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
② 全局唯一id = 当前时间戳【高 32 位】+ redis 自增长序列【低 32 位】
//3.拼接并返回
private static final int COUNT_BITS = 32;
return timestamp << COUNT_BITS | count;
完整代码:
@Component
public class RedisIdWorker {
// 开始时间戳
private static final long BEGIN_TIMESTAMP = 1648857600L;
// 序列号的位数
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 自增长:redis 自增长key: 前缀 + 当前日期
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
实现效果:
实现效果分析:高 32 位为 时间戳,低 32 位为 redis 中精确到 天的记录。同一天的记录可以根据时间戳前缀唯一标识,同时在redis 中可以直观看到与相关业务逻辑以及日期相关的信息
流程说明:
① 判断秒杀是否开始或结束,若尚未开始或已经结束则无法下单
② 库存是否充足,不足则无法下单
① 扣减库存
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
② 创建订单并保存到数据库
// 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);
使用 postman添加优惠券:如下图所示,在 2 号商铺中添加优惠券
通过 postman 添加优惠券后
会将优惠券信息同时写入 tb_voucher 和 tb_seckill_voucher 表中
tb_voucher :记录优惠券的店铺,描述,面值等信息
tb_seckill_voucher :记录优惠券秒杀开始,结束时间以及库存
实现过程:
① 通过 postman 发送请求到 /voucher/seckill
请求被分发到 Controller 层的 addSeckillVoucher(@RequestBody Voucher voucher)
@RestController
@RequestMapping("/voucher")
public class VoucherController {
/**
* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
}
② Service 层 VoucherServiceImpl.java 中的 addSeckillVouocher(Voucher voucher) 实现保存秒杀券信息到数据库中
@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券
save(voucher);
// 保存秒杀信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
}
优惠券秒杀核心代码:
// VoucherOrderServiceImpl.java
@Override
@Transactional
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);
// 7.返回订单 id
return Result.ok(orderId);
}
视频地址
100件库存,200左右人抢购,数据库中库存为负数
原因分析:
// VoucherOrderServiceImpl.java -- seckillVoucher(Long voucherId)
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足!");
}
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
库存超卖 流程演示:
对比可知:流程二出现异常主要是在 查询库存和 扣减库存期间有线程查询库存,取到了库存扣减之前的值。判断条件【库存是否充足】失效。而这在高并发场景下是不可避免的
解决方案:
① 悲观锁:synchronized,Lock 操作数据前先获取锁,确保线程串行执行
② 乐观锁:CAS,修改前判断是否与预期相符,相符则更新数据
问题:如何用 CAS 解决超卖?
分析:CAS 的关键是比较当前值与预期值是否相符,如何设置预期值呢?
① 数据库中添加一个 version 字段,执行查询逻辑时,从数据库中取出 stock 和 version ;执行扣减逻辑时,判断当前 version 是否与从数据库查询到的一致。一致说明没有其他线程操作,可以扣减,否则已被其他线程修改,扣减失败
② 优化:将 version 的功能合并到 stock 上,比较时的预期值为从数据库中查询的 stock 值,扣减前 stock 没有变化,说明没有线程修改,可以执行扣减
代码实现:在操作数据库执行扣减时,添加判断条件,stock 是否与查询到的值一致。
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.eq("stock", voucher.getStock())
.update();
测试:利用 Jmeter 创建 200 个线程,抢购库存为100的优惠券【id = 18】
测试结果:库存剩余 74 件,错误率高达90%
问题分析:多个线程查询到相同的库存,但只有一个执行CAS成功
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
修改后的测试结果:
① 数据库中的数据扣减为0
② JMeter 失败率 50 【200人抢购库存为100的商品】
一人一单:同一优惠券,一个用户只能下一单
思路:下单之前判断秒杀券订单表中是否有该用户信息,若有则抢购失败
步骤:
① 根据ThreadLocal 查询当前线程的用户信息
② 根据用户 id 查询 tb_voucher_order 中的用户抢购的优惠券【正在被秒杀的优惠券】的数目
③ 若用户已有当前优惠券的抢购订单信息,则抢购失败,否则添加订单到数据库
代码实现:
// 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();
测试:
测试说明 :JMeter 中创建200 线程,设置登录状态头,模拟同一用户下单200次场景
测试结果:stock 减为 90,即同一用户下了 10 单
问题分析:与库存超卖的问题相同,实现一人一单的逻辑也是:先查询订单,然后再创建订单。同一用户多次操作模拟并发场景,多次操作查询到用户下单为0,然后执行创建订单业务
解决方案:加锁
问题:能否用乐观锁执行?
不能,原因是乐观锁只能操作单个变量,而创建订单需要操作数据库
@Transactional
public Result createVoucherOrder(Long voucherId){
// 5. 一人一单
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")
.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);
}
}
① 同一个线程每次请求获取的 userId 是否是一样的?
Long userId = UserHolder.getUser().getId();
返回值为 Long 类型,即属于对象类型
问题转化为:每次从 ThreadLocal 中获取存储的对象是否是同一个对象?
// ThreadLocal.java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal 中通过 get() 方法找到对应的 Entry 后执行了 T result = (T) e.value
最后将 result 作为结果返回
那么 result 和 map 中存储的 value 之间是什么关系呢?
① 由于 result 是新创建的变量,result 和 value 必然不是同一个对象
② result 对象引用,被赋值为 value 的引用地址
③ 故 result 和 value 不是同一个对象,但是内容相同,指向同一块地址
由于 synchronized 代码块的锁为( )
内的对象,每次请求时得到的 userId 不是同一个对象,必然是不同的锁,因此需要比较其数值。
那么为什么不转为基本类型,而是转为 String 呢?
因为 synchronized 锁为对象,基本类型不属于引用类型
String 类是不可变的,对String 操作都会返回新的 String 对象
// Long.java
public String toString() {
return toString(value);
}
public static String toString(long i) {
if (i == Long.MIN_VALUE)
return "-9223372036854775808";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
即:每次toString() 之后生成的是不同的 String 对象
那么 intern( ) 的作用是什么呢?
// String.java
public native String intern();
深入理解JVM 第 3 版 pg 61 - pg 63
2.4.3 方法区和运行时常量池溢出
① intern( ) :操作的是字符串字面量,即下面的 “abc”
String s1 = new String("abc");
s1 = s1.intern();
String s2 = "abc";
System.out.println(s1 == s2); // true
② JDK 7 之后,String::intern( ) 会返回 字符串字面量 “abc” 在字符串常量池中的引用,若没有就添加并返回
上述示例中:s2 指向字符串常量池中 “abc” 的引用,s1 执行 intern( ) 会返回字符串常量池中 “abc” 的引用,因此 s1 == s2
测试 :
测试说明: JMeter 下 同一用户创建 200 线程抢购库存为 100 的优惠券 【id = 18】
测试结果:正确,只扣减了一条记录
集群下的线程并发安全问题
问题复现:
① 在IDEA 上开启两个 JVM 进程
② Nginx 中的 nginx.conf 文件中使用 upstream 配置服务组进行负载均衡
③ 在postman上使用同一用户发送两次秒杀请求
测试数据未保存,从视频上截取的结果
tb_voucher_order 表中同一用户 user_id,抢购了两次voucher_id = 7 的优惠券
出现上述问题的原因:
① 不同的服务启动不同的 JVM,
② synchronzied 只能保证单个 JVM 内部的多个线程互斥
即 synchronized 作用于跨进程的 线程时是失效的
什么是分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
实现分布式锁的常见方式?
① MySQL 自带的互斥锁机制 ???【暂未了解】
② Redis 利用 setnex 互斥命令
③ Zookeeper 利用节点的唯一性和有序性 【不了解】
视频:综合来说 使用 redis 效果最佳
so so,后续再了解
核心代码实现逻辑:
① redis 实现分布式锁的两个基本方法
# 添加锁,利用 setnx 的互斥特性
SETNX lock thread1
# 添加锁过期时间,避免服务宕机引起死锁
EXPIRE lock 10
# 释放锁,删除 key
DEL key
② 利用 StringRedisTemplate中提供的 setIfAbsent(…) 和 delete(…) 获取锁和释放锁
@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);
}
完整代码实现:
① 定义 ILock 接口声明 tryLock(time) ,unlock() 方法
public interface ILock {
boolean tryLock(long timeoutSec);
void unlock();
}
② 定义 SimpleRedisLock 实现类,实现 tryLock(time),unlock() 方法
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) {
// 获取线程标识
long threadId = Thread.currentThread().getId();
// 非阻塞获取锁
// key--lock:order value--threadId
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);
}
}
③ 修改一人一单逻辑:使用自定义锁 替换 synchronized
@Resource
private StringRedisTemplate stringRedisTemplate;
@Transactional
public Result createVoucherOrder(Long voucherId){
// 5. 一人一单
Long userId = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//尝试获得锁
boolean isLock = redisLock.tryLock(1200);
//判断
if(!isLock){
//获取锁失败,直接返回失败或重试
return Result.fail("不允许重复下单");
}
//获取锁成功,执行一人一单的逻辑
try {
//一人一单/
// 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);
} finally {
// 释放锁
redisLock.unlock();
}
}
Redis深度历险 pg 18
① setnx(set if not exists) 只会有一个线程获取锁
② del 释放锁
③ expire :给锁设置过期时间,即使 del 指令因故障没有执行,锁仍然会被释放
④ set lock ex time nx :将 setnx 和 expire 组合成原子指令,避免expire 得不到执行时,造成死锁
上述是redis 分布式锁的演化过程,那么进行到第④步,将加锁操作和设置过期时间的操作打包成原子指令后,还会出现什么问题?
当业务阻塞时,锁会被超时释放。在高并发场景下,会出现锁误删问题
什么是分布式锁误删?
① 线程1 由于业务阻塞,线程1持有的锁因超时被释放
② 线程2 获取到线程1 超时释放的锁
③ 线程1 阻塞恢复,执行释放锁的逻辑,释放掉线程2 持有的锁
④ 线程3 获取被线程1 释放的锁
最终导致的结果:线程2 线程3 同时进入临界区执行任务
问题根源:线程的锁被超时释放,线程在执行释放锁的逻辑时删除了其他线程持有的锁
问题分析:误删的根源是删除锁的时候没有判断条件,任意线程都可能删除别的线程的锁,因此需要设定条件线程只能删除自己获得的锁
核心问题:在释放线程的时候,区分释放锁的线程
问题:如何区分线程?注意是在分布式存在多个JVM的情况下
不仅要区分不同的线程,还要区分不同 JVM上的线程
解决方案:使用 uuid + threadid 拼接字符串标识不同JVM上的不同线程
① 使用 UUID 区分不同的服务 JVM
② 使用 线程ID 区分JVM 的不同线程
** 代码实现**:
① 获取锁时存入线程标识
// 锁 key 前缀
private static final String KEY_PREFIX = "lock:";
// 线程标识前缀
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);
}
② 释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
@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);
}
}
分布式锁误删的原因是:删除锁的时候若不进行线程判断,会删除其他线程持有的锁,导致多个线程并行
但是上述判断 锁标识 和释放锁的操作不是原子的,根据分布式锁组合 setnx 和 expire 指令的背景知识可知。当判断锁标识和释放锁不是原子操作时,会出现问题
会出现什么问题呢?
本图加了锁标识判断后的流程和没加之前基本一致,最后都导致了线程2,3并行
问题分析:
① 线程1 判断锁标识通过后,在删除锁的过程中发生阻塞
② 线程1 的锁再次出现超时释放,线程2获取锁
③ 线程2 持有锁期间,线程1 解除阻塞,由于经过了锁标识判断的检查,线程1 直接执行删除锁的逻辑,再次删除了线程2 的锁
④ 线程3 获得被线程1 释放的锁,线程2,3并行
问题的根源:判断锁标识和释放锁不是原子操作
解决方案:使用 lua 脚本将两条指令打包成原子操作
问题:使用 lua 脚本的原因?
将 redis 操作中判断锁标识的操作,和释放锁的操作整合册成原子操作
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
上述代码可以拆分为两部分:
① lua 的流程控制语法
--[ 0 为 true ]
if(0)
then
print("0 为 true")
end
② lua 脚本调用redis
-- 执行 redis 命令
redis.call('命令名称','key','参数'...)
-- exam: 执行 set name jack
redis.call('set','name','jack')
代码实现:
① 在 resources 目录下创建 unlock.lua
上述 lua 脚本代码执行流程说明:
② 修改 unlock() 代码逻辑,使用 stringRedisTemplate.execute 执行 lua 脚本
// SimpleRedisLock.java
//借助 DefaultRedisScript 使得 lua 脚本在启动时加载,且仅加载一次即可
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static{
// 构造函数初始化 UNLOCK_SCRIPT 对象
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// lua 脚本路径
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// 返回值类型【lua脚本的返回值】
UNLOCK_SCRIPT.setResultType(Long.class);
}
// 使用 lua 脚本 判断锁标识 和释放锁
@Override
public void unlock(){
// 调用 lua 脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT, // lua 脚本
Collections.singletonList(KEY_PREFIX + name), // KEYS[1] 锁
ID_PREFIX + Thread.currentThread().getId()); // ARGV[1] 线程标识
}
上述基于 setnx 实现的分布式锁存在的问题:
① 不可重入
② 不可重试
③ 超时释放:锁超时释放可避免死锁,但若是耗时业务也会导致锁释放
④ 主从一致性:从节点还未和主节点同步,主节点发生宕机
解决方案:redisson 实现分布式锁
使用 redisson 实现分布式锁的流程
① 配置 pom.xml 文件
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.13.6version>
dependency>
② config 目录下添加 RedissonConfig.java 配置类
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//配置
Config config = new Config(); config.useSingleServer().setAddress("redis://xxx.xxx.xxx.xxx:6379").setPassword("password");
//创建 RedissonClient 对象
return Redisson.create(config);
}
}
③ createVoucherOrder(…) 方法中使用 redissonClient 提供的 getLock(),unlock() 方法,替代自定义接口实现类实现的 tryLock(),unlock() 方法
// VoucherOrderServiceImpl.java
@Resource
private RedissonClient redissonClient;
@Transactional
public Result createVoucherOrder(Long voucherId){
// 5. 一人一单
Long userId = UserHolder.getUser().getId();
//创建锁对象
RLock redisLock = redissonClient.getLock("lock:order:" + userId);
//尝试获得锁
boolean isLock = redisLock.tryLock();
//判断
if(!isLock){
//获取锁失败,直接返回失败或重试
return Result.fail("不允许重复下单");
}
try {
// 5.1 查询订单
...
// 6.扣减库存
...
// 7.创建订单
...
} finally {
redisLock.unlock();
}
}
异步秒杀思路:将判断秒杀库存及校验一人一单的工作放到 redis 中,
与操作数据库耗时较久的 扣减库存和创建订单操作分开,中间借助 消息队列,异步读取队列中的信息,完成下单
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券到 tb_voucher
save(voucher);
// 保存秒杀信息到 tb_seckill_voucher
...
seckillVoucherService.save(seckillVoucher);
// 保存秒杀库存到 Redis 中
stringRedisTemplate.opsForValue()
.set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
lua 脚本的执行逻辑:
① 获取:优惠券id 【voucherId】,用户id【userId】,订单id【orderId】
-- 1.1 优惠券 id
local voucherId = ARGV[1]
-- 1.2 用户 id
local userId = ARGV[2]
② 拼接:库存key【stockKey】,订单key【orderKey】通过 voucherId拼接
-- 2. 数据 key
-- 2.1 库存 key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单 key
local orderKey = 'seckill:order:' .. voucherId
③ 判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2 库存不足,返回 1
return 1
end
④ 判断用户是否下单 sismember orderkey userId【订单列表中是否有用户id】
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3 存在,说明重复下单,返回 2
return 2
end
⑤ 扣减库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
⑥ 下单(保存用户) sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
//使用 @PostConstruct 注解方法,在对象加载完依赖注入后执行,会在服务器加载 Servlet 的时候运行,且只运行一次
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while(true){
try {
// 1.获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take();
// 2.创建订单
createVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
阻塞队列执行逻辑:
① 创建阻塞队列用户存放订单信息
② 创建线程池执行阻塞队列中的任务
// VoucherOrderServiceImpl.java
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
// 1.执行 lua 脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString()
);
int r = result.intValue();
// 2.判断结果是否为 0
if(r != 0){
// 2.1不为0,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 2.2为0,有购买资格,把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
// 2.3 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 2.4 用户id
voucherOrder.setUserId(userId);
// 2.5 代金券id
voucherOrder.setVoucherId(voucherId);
// 2.6 放入阻塞队列
orderTasks.add(voucherOrder);
// 3.返回订单 id
return Result.ok(orderId);
}