【黑马点评-3秒杀优惠券】零、RabbitMQ + lua
1 Redis实现的Demo分布式锁的问题
- 分布式锁不可重入:
- 不可重入是指同一线程不能重复获取同一把锁。
- 比如,方法A中调用方法B,方法A需要获取分布式锁,方法B同样需要获取分布式锁,线程1进入方法A获取了一次锁,进入方法B又获取一次锁,由于锁不可重入,所以就会导致死锁
- 分布式锁不可重试:
- 获取锁只尝试一次就返回false,没有重试机制,这会导致数据丢失,
- 比如线程1获取锁,然后要将数据写入数据库,但是当前的锁被线程2占用了,线程1直接就结束了而不去重试,这就导致数据发生了丢失
- 分布式锁超时释放:
- 超时释放机制虽然一定程度避免了死锁发生的概率,但是如果业务执行耗时过长,期间锁就释放了,这样存在安全隐患。
- 锁的有效期过短,容易出现业务没执行完就被释放,
- 锁的有效期过长,容易出现死锁,所以这是一个大难题!
- 我们可以设置一个较短的有效期,但是加上一个 心跳机制 和 自动续期:(以下尚未理解)
- 在锁被获取后,可以使用心跳机制并自动续期锁的持有时间。通过定期发送心跳请求,显示地告知其他线程或系统锁还在使用中,同时更新锁的过期时间。
- 如果某个线程持有锁的时间超过了预设的有效时间,其他线程可以尝试重新获取锁。
- 主从一致性问题:如果Redis提供了主从集群,主从同步存在延迟,?
解决:Redssion是一个十分成熟的Redis框架,功能也很多,比如:分布式锁和同步器、分布式对象、分布式集合、分布式服务,各种Redis实现分布式的解决方案。
2 代码逻辑
1. 使用Guava库的RateLimiter,通过限制1s 10个许可的方式,限制请求
- 设置1s只发10 个许可
- 在不超过指定超时时间1s的情况下获得许可
private final RateLimiter rateLimiter = RateLimiter.create(10);
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
return Result.fail("目前网络正忙,请重试");
}
2. 通过ThreadLocal获取当前用户id
从ThreadLocal中获取UserDTO中的属性id
UserDTO的三个属性:id、nickname、icon
Long userId = UserHolder.getUser().getId();
3. 执行lua脚本
- excute的三个参数
- DefaultRedisScript封装的要执行的脚本
- 空集合,填充用的,因为用的args传的
- voucherId(方法参数) + userId(ThreadLocal里获得的)
Long r = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString()
);
4. 根据lua脚本的结果,判断
- 获取优惠券id,用户id
- 用redis的get命令,判断优惠券id小于0么,小于失败返回1
- 判断这个用户获取了这个优惠券么,用set判断,属于,失败返回2
- 都不失败,则order-1, 将用户id保存到oderKey对应的set里
-- 1.参数列表
-- 1.1优惠卷id
local voucherId = ARGV[1]
-- 1.2用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1判断库存是否充足
if(tonumber(redis.call('get',stockKey)) <= 0)then
-- 3.2 库存不足 返回1
return 1
end
--3.2判断用户是否下单
if(redis.call('sismember',orderKey,userId) == 1) then
-- 3.3存在,说明是重复下单
return 2
end
-- 3.4扣库存
redis.call('incrby',stockKey,-1)
-- 3.5下单并保存用户
redis.call('sadd',orderKey,userId)
return 0
5. 若lua脚本允许下单,创建VoucherOrder实体,存储相关信息
VoucherOrder的字段如下
- id
- userId
- voucherId
long orderId = redisIdWorker.nextId("order");
// 设置订单属性: 将生成的订单 ID、用户 ID 和代金券 ID 设置到 voucherOrder 对象中。
voucherOrder.setId(orderId);
// 用户id
voucherOrder.setUserId(userId);
// 代金卷id
voucherOrder.setVoucherId(voucherId);
6. 用RabbitMQ发送秒杀信息,返回订单id,成功
mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
// 返回订单id
return Result.ok(orderId);
3 总结
基于Redis的分布式锁实现思路:
- 利用set nx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
4 示例代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder>
implements IVoucherOrderService {
/*
* 1. 定义一个静态常量 SECKILL_SCRIPT,用于存储 Redis 脚本
* 使用 DefaultRedisScript<Long> 类型,表示该脚本执行后返回值的类型为 Long。
*/
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
/*
* 2. 静态初始化块:
* 在静态初始化块中,对 SECKILL_SCRIPT 进行实例化和配置。
*/
static {
/*
* 实例化 DefaultRedisScript:
* 创建 DefaultRedisScript 的实例,用于封装将要执行的 Lua 脚本。
* DefaultRedisScript 是 Spring Data Redis 提供的一个类,用于在 Redis 中执行脚本。
*/
SECKILL_SCRIPT = new DefaultRedisScript<>();
/*
* 设置脚本位置:
* setLocation 方法用于指定脚本的位置。这里使用了 ClassPathResource,表示脚本位于类路径下的 seckill.lua 文件。
* ClassPathResource 是 Spring 提供的一个类,用于访问类路径下的资源文件。
*/
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
/*
* setResultType 方法用于指定脚本执行后的返回值类型,这里设置为 Long.class,表示脚本返回的是一个长整型值。
*/
SECKILL_SCRIPT.setResultType(Long.class);
}
/**
* RateLimiter 的使用: rateLimiter 是基于 Google Guava 库中的 RateLimiter 类实例,
* 采用令牌桶算法进行限流。
* RateLimiter.create(10) 表示每秒生成 10 个令牌,即每秒允许最多 10 个请求通过。
*/
private final RateLimiter rateLimiter = RateLimiter.create(10);
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private MQSender mqSender;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
/*
* tryAcquire 方法: 尝试在指定时间内获取一个令牌。这里设置了超时时间为 1000 毫秒(1 秒)。
* 如果在超时时间内未能获取到令牌,说明当前请求过多,返回错误信息 "目前网络正忙,请重试"。
*/
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
return Result.fail("目前网络正忙,请重试");
}
/*
* 1.执行lua脚本
* 获取用户 ID: 通过 UserHolder.getUser().getId() 获取当前登录用户的 ID。
*/
Long userId = UserHolder.getUser().getId();
/*
* 执行 Lua 脚本: 使用 stringRedisTemplate 的 execute 方法执行预先定义的 Lua 脚本 SECKILL_SCRIPT。
* 该脚本的主要功能包括:
* 检查代金券库存是否充足。
* 验证用户是否已经参与过此次秒杀(防止重复下单)。
* 如果上述条件满足,扣减库存并记录用户的秒杀资格。
* Lua 脚本的执行保证了上述操作的原子性,避免了并发问题。
*/
Long r = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString());
/*
* 2.判断结果
* 结果判断: 将 Lua 脚本的返回值转换为整数 result。根据约定:
* 返回 0 表示秒杀成功。
* 返回 1 表示库存不足。
* 返回 2 表示用户重复下单。
* 处理失败情况: 如果 result 不为 0,根据返回值提供相应的错误提示信息。
*/
int result = r.intValue();
if (result != 0) {
// 2.1不为0代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "该用户重复下单");
}
// 2.2为0代表有购买资格,将下单信息保存到阻塞队列
// 3. 创建订单并发送消息
// 订单对象创建: 如果秒杀资格验证通过,创建一个新的 VoucherOrder 对象。
VoucherOrder voucherOrder = new VoucherOrder();
/*
* 生成订单 ID: 使用 redisIdWorker.nextId("order") 生成全局唯一的订单 ID。
* redisIdWorker 是一个基于 Redis 实现的分布式 ID 生成器,确保在分布式环境下生成唯一的 ID。
*/
long orderId = redisIdWorker.nextId("order");
// 设置订单属性: 将生成的订单 ID、用户 ID 和代金券 ID 设置到 voucherOrder 对象中。
voucherOrder.setId(orderId);
// 用户id
voucherOrder.setUserId(userId);
// 代金卷id
voucherOrder.setVoucherId(voucherId);
/*
* 发送消息: 将 voucherOrder 对象转换为 JSON 字符串,
* 将信息放入MQ中
*/
mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
// 返回订单id
return Result.ok();
}
}

浙公网安备 33010602011771号