一,全局唯一ID
全局ID生成器: 在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性:
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
全局唯一ID生成策略:
- UUID:生成的是16进制数值,返回的是String,不是单调递增的特性
- Redis自增
- snowflake雪花算法:不依赖redis,但是对系统时间依赖很高,如果系统时间不正确会出现异常情况
- 数据库自增:使用数据库特定表来实现自增
为了增加ID的安全性,我们不可以直接使用Redis自增的数值,而是拼接一些其他信息: 代码实现:
java
@Component
public class RedisIdWorker {
private static final long BGIN_TIMESTAP=LocalDateTime.of(2022, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);
//序列号的位数
private static final int COUNT_BITS=32;
private StringRedisTemplate redisTemplate;
public RedisIdWorker(StringRedisTemplate redisTemplate){
this.redisTemplate=redisTemplate;
}
public long nextId(String keyPrefix){
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp=nowSecond-BGIN_TIMESTAP;
//2.生成序列号
//获取当前日期
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count=redisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);
//3.拼接并返回
//先将时间戳左移32位成为高位,然后用|运算将count值原封不动填充进低位
//因为|运算:有一个为1就为1,而此时低位全是0,我们的count就会原封不动的进入低位
return timestamp << COUNT_BITS | count;
}
}
二,实现优惠卷秒杀下单
-
优惠卷表结构
字段 备注 id 主键 shop_id 商铺id title 代金券标题 sub_title 副标题 rules 使用规则 pay_value 支付金额,单位是分。例如200代表2元 actual_value 抵扣金额,单位是分。例如200代表2元 type 类型0,普通券;1,秒杀券 status 状态1,上架; 2,下架; 3,过期 create_time 创建时间 update_time 更新时间 -
下单表表结构
字段 备注 id 主键 user_id 下单的用户id voucher_id 购买的代金券id(关联优惠卷id) pay_type 支付方式 1:余额支付;2:支付宝;3:微信 status 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款 create_time 下单时间 pay_time 支付时间 use_time 核销时间 refund_time 退款时间 update_time 更新时间 -
秒杀卷表表结构
字段 备注 voucher_id 关联的优惠券的id stock 库存 create_time 创建时间 begin_time 生效时间 end_time 失效时间 update_time 更新时间

代码实现:
java
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1.查询优惠卷信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
log.info("data={}",voucher);
//2.判断当前时间是否是秒杀时间
boolean isAfter = voucher.getBeginTime().isAfter(LocalDateTime.now());
log.info("isAfter={}",isAfter);
if(isAfter){
return Result.fail("秒杀尚未开始!");
}
boolean isBefore = voucher.getEndTime().isBefore(LocalDateTime.now());
log.info("isBefore={}",isBefore);
if(isBefore){
return Result.fail("秒杀已结束!");
}
//3.开始,判断库存是否充足
Integer stock = voucher.getStock();
if(stock<1){
//不足
return Result.fail("库存不足!");
}
//4.充足,扣减库存
boolean success=seckillVoucherService.update().setSql("stock=stock-1")
.eq("voucher_id",voucherId)
.update();
if(!success){
return Result.fail("库存不足!");
}
//5.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金卷id
voucherOrder.setVoucherId(voucherId);
//保存订单到数据库
save(voucherOrder);
//6.返回订单id
return Result.ok(orderId);
}
}
三,超卖问题
乐观锁解决超卖问题:


乐观锁如果直接按照上述方按进行,会导致大部分的请求失败,我们只需要stock>0即可更新,而不是stock必须严格的等于原来的stock,这样就可以改进。
版本号法代码实现:
java
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1.查询优惠卷信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
log.info("data={}",voucher);
//2.判断当前时间是否是秒杀时间
boolean isAfter = voucher.getBeginTime().isAfter(LocalDateTime.now());
log.info("isAfter={}",isAfter);
if(isAfter){
return Result.fail("秒杀尚未开始!");
}
boolean isBefore = voucher.getEndTime().isBefore(LocalDateTime.now());
log.info("isBefore={}",isBefore);
if(isBefore){
return Result.fail("秒杀已结束!");
}
//3.开始,判断库存是否充足
Integer stock = voucher.getStock();
if(stock<1){
//不足
return Result.fail("库存不足!");
}
//4.充足,扣减库存
boolean success=seckillVoucherService.update().setSql("stock=stock-1")
.eq("voucher_id",voucherId)
.gt("stock",0)//这里就是stock>0
.update();
if(!success){
return Result.fail("库存不足!");
}
//5.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//6.返回订单id
return Result.ok(orderId);
}
}
四,一人一单

代码实现
java
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
private final Lock lock=new ReentrantLock();
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠卷信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
log.info("data={}",voucher);
//2.判断当前时间是否是秒杀时间
boolean isAfter = voucher.getBeginTime().isAfter(LocalDateTime.now());
log.info("isAfter={}",isAfter);
if(isAfter){
return Result.fail("秒杀尚未开始!");
}
boolean isBefore = voucher.getEndTime().isBefore(LocalDateTime.now());
log.info("isBefore={}",isBefore);
if(isBefore){
return Result.fail("秒杀已结束!");
}
//3.开始,判断库存是否充足
Integer stock = voucher.getStock();
if(stock<1){
//不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
//新建订单必须加锁,不然会出现脏读的情况
synchronized (userId.toString().intern()){
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucher(voucherId,userId);
}
}
@Transactional
@Override
public Result createVoucher(Long voucherId, Long userId) {
//4.根据优惠券id和用户id查询订单
int count=query().eq("user_id",userId).eq("voucher_id",voucherId).count();
//5.判断订单是否存在
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();
//订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//用户id
voucherOrder.setUserId(userId);
//代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8.返回订单id
return Result.ok(orderId);
}
}
上述代码涉及到好几个知识点:
-
锁的范围必须在事务之前,如果锁加到了事务方法里面,事务是必须等方法执行完才能提交的,如果此时锁先释放了,事务未提交,其他的线程就已经进来了,我们刚新增的订单可能还未写入数据库,此时新线程查询订单时订单依旧不存在,这会导致线程并发安全问题。
-
事务要想生效是因为Spring对当前这个类做了动态代理 ,拿到了代理对象,用代理对象来进行事务处理,如果我们不用proxy手动获取一个代理对象,而是直接执行方法,相当于使用this来执行这个方法,而this是非代理对象,这样会导致事务失效 ,解决方法就是如上代码手动创建一个代理对象,记得在启动类开启暴露代理对象:
@EnableAspectJAutoProxy(exposeProxy = true)
然后添加依赖:xml<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
五,分布式锁
上述情况通过加锁可以解决在单机情况 下的一人一单安全问题,但是在集群模式 下就不行了,在集群模式下,每个JVM都有自己的锁 ,导致线程安全问题。
分布式锁的工作原理 :满足分布式系统或集群模式下多进程可见 并互斥的锁。 分布式锁基本特性:
- 多进程可见
- 互斥
- 高可用
- 高性能
- 安全性
5.1 分布式锁的实现

**基于Redis的分布式锁:**实现分布式锁需要实现两个基本方法
-
获取锁: 互斥:利用
setnx
的互斥特性非阻塞:实现非阻塞式锁
bashSETNX lock thread1 EXPIRE lock 10 #或者直接set命令 set lock NX EX 10
-
释放锁:
bashDEL lock
-
流程图:
5.2 基于Redis实现分布式锁初级版本
java
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX="lock:";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long timeoutSec) {
long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
//涉及自动拆箱,可能为null
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
手动上锁解锁代码
java
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.createVoucher(voucherId,userId);
}finally {
lock.unLock();
}
5.3 解决锁误删问题
上述初级版本存在一个,锁误删问题,如图: 解决办法 :解锁时判断锁标识是否一致
流程图:
代码实现:核心就是加上线程标识,阻止其他线程误删除锁。
java
public class SimpleRedisLock implements ILock{
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true);
public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@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);
//涉及自动拆箱,可能为null
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);
}
}
}
5.4 分布式锁的原子性(lua脚本)
我们上述解决锁误删问题中,判断锁和解锁是两个动作,如果两个动作之间发生了阻塞,也会导致锁误删。如下图: 解决办法:保证判断锁和解锁两个操作是一起执行的,即原子性。
Lua脚本解决多条命令的原子性
-
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
-
lua脚本编写
lua-- 获取锁中线程标识 local id=redis.call('get',KEYS[1]) -- 比较线程标识与锁的标识是否一致 if(id == ARGV[1]) then --一致,释放锁 return redis.call('del',KEYS[1]) end -- 不一致,返回0 return 0
基于Lua脚本实现分布式锁的释放逻辑
java
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static{
UNLOCK_SCRIPT=new DefaultRedisScript<>();
//ClassPathResource是Spring提供的,在classpath即resource目录下找
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unLock.lua"));
//设置返回值
UNLOCK_SCRIPT.setResultType(Long.class);
}
@Override
public void unLock() {
//获取线程标识
String threadId = ID_PREFIX+":"+Thread.currentThread().getId();
//调用lua脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
threadId);
}
5.5 Redisson
5.5.1 Redisson介绍
5.5.2 Redisson快速入门:
-
引入依赖
xml<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.20.1</version> </dependency>
-
配置Redisson客户端
java@Configuration public class RedisConfig { @Value("${redis.host}") private String host; @Value("${redis.port}") private String port; @Value("${redis.password}") private String password; @Bean public RedissonClient redissonClient(){ //配置类 Config config = new Config(); //添加redis地址,这里添加了单点地址,也可以使用config.useClusterServer()添加集群地址 config.useSingleServer().setAddress("redis://"+host+":"+port).setPassword(password); return Redisson.create(config); } }
-
利用Redisson的分布式锁
java@SpringBootTest class HmDianPingApplicationTests { @Autowired private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException { //获取锁(可重入),指定锁的名称 RLock lock = redissonClient.getLock("anyLock"); //尝试获取锁,参数分别是: //获取锁的最大等待时间,锁自动释放时间,时间单位 //如果不填最大等待时间,那么获取不到锁就返回false //如果添了最大等待时间,就会不断尝试直到获取到锁或到达最大等待时间 //锁自动释放时间如果不填默认-1 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); //判断释放成功 if(isLock){ try{ System.out.println("执行业务!"); }finally { //释放锁 lock.unlock(); } } } }
使用Redisson锁替换我们自定义锁步骤:替换锁对象即可。
5.5.3 Redisson可重入锁原理
5.5.4 Redisson的锁重试和Redisson的WatchDog机制

5.5.5 Redisson分布式锁主从一致性问题
解决办法:Redisson的MultiLock
5.6 总结
三种锁分布式锁:
- 不可重入Redis分布式锁
- 原理:利用setnx的互斥性,理由ex避免死锁,释放锁时判断线程标识。
- 缺线:不可重入,无法重试,锁超时失效。
- 可重入Redis分布式锁
- 原理:利用Hash结构,记录线程标识和重入次数,利用watchDog延续锁时间,理由信号量控制锁重试等待。
- 缺线:主从集群的redis宕机引起锁失效问题
- Redisson的multiLock:
- 原理:多个独立的Redis节点,必须在所有节点都获取到重入锁才算成功
- 缺点:运维成本高,实现复杂。
六,Redis优化秒杀
未优化前秒杀流程: 优化思路:
流程图:
代码实现:
-
新增秒杀优惠券的同时,将优惠券库存信息保存到Redis中
java@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); //保存秒杀库存到Redis中 stringRedisTemplate.opsForValue().set("seckill:stock:"+voucher.getId(),voucher.getStock().toString()); }
-
基于Lua脚本,判断秒杀库存,一人一单,决定用户是否抢购成功
- Lua脚本
lua--1. 参数列表 -- 优惠券id local voucherId=ARGV[1] -- 用户id local userId=ARGV[2] --2. 数据key -- 库存key local stockKey='seckill:stock:' .. voucherId -- 订单key local orderKey='seckill:order:' .. voucherId --3. 脚本业务 -- 判断库存是否充足 get stockKey if(tonumber(redis.call('get',stockKey)) <= 0) then --库存不足,返回1 return 1 end -- 判断用户是否下单 if(redis.call('sismember',orderKey,userId) ==1) then -- 存在,说明是重复下单,返回2 return 2 end -- 扣库存 redis.call('incrby',stockKey,-1) -- 下单(保存用户) redis.call('sadd',orderKey,userId)
-
如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
java//创建IVoucherOrderService代理对象 private IVoucherOrderService proxy; //阻塞队列的特点:当一个线程尝试从阻塞队列里面获取元素,如果里没有元素,这个线程就会被阻塞 private BlockingQueue<VoucherOrder> orderTasks=newArrayBlockingQueue<>(1024*1024); @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()); //2. 判断结果是否为0 int r = result.intValue(); if( r!= 0){ //不为0,代表没有购买资格 return Result.fail(r == 1 ? "库存不足":"不能重复下单"); } //为0,有购买资格,把下单信息保存到阻塞队列中 long orderId = redisIdWorker.nextId("order"); //创建订单 VoucherOrder voucherOrder = new VoucherOrder(); //订单id voucherOrder.setId(orderId); //用户id voucherOrder.setUserId(userId); //代金卷id voucherOrder.setVoucherId(voucherId); save(voucherOrder); orderTasks.add(voucherOrder); //获取代理对象 proxy =(IVoucherOrderService) AopContext.currentProxy(); //返回订单id return Result.ok(orderId); }
-
开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
java//创建线程池,这里是示例,实际上不推荐用这种方法创建线程池因为这种方法会导致OOM private static final ExecutorServiceSECKILL_ORDER_EXECUTOR=Executors.newSingleThreadExecutor(); //异步任务 private class VoucherOrderHandler implements Runnable{ @Override public void run() { while(true){ try{ //1.获取队列中的订单信息 VoucherOrder voucherOrder = orderTasks.take(); //2.处理订单 handleVoucherOrder(voucherOrder); }catch (Exception e){ log.info("订单异常",e); } } } private void handleVoucherOrder(VoucherOrder voucherOrder) throws InterruptedException { //1.获取用户 Long userId = voucherOrder.getUserId(); //2.创建锁对象 RLock lock = redissonClient.getLock("lock:order:" + userId); //获取锁(waitTime:获取锁的最大等待时长,给了这个参数那么获 取锁不成功就会不断地去尝试,不给就是直接返回false) //leaseTime:锁失效的时间,即TTL boolean isLock = lock.tryLock(10L, TimeUnit.MINUTES); //判断锁是否获取成功 if(!isLock){ //不成功 log.info("不允许重复下单"); return; } try{ //创建订单 proxy.createVoucher(voucherOrder); }finally { lock.unlock(); } } } @PostConstruct private void init(){ //将任务提交给线程池 SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); } //保存订单 @Transactional @Override public void createVoucher(VoucherOrder voucherOrder) { Long userId = voucherOrder.getUserId(); //1.根据优惠券id和用户id查询订单 int count=query().eq("user_id",userId).eq("voucher_id", voucherOrder.getVoucherId()).count(); //2.判断订单是否存在 if(count > 0) { //存在 log.info("用户已经购买过了!"); return; } //3.订单不存在,扣减库存 boolean success=seckillVoucherService.update().setSql ("stock=stock-1") .eq("voucher_id",voucherOrder.getVoucherId()) .gt("stock",0) .update(); if(!success){ log.info("库存不足"); return; } //4.保存 save(voucherOrder); }
七,Redis消息队列实现异步秒杀
消息队列,字面意思就是存放信息的队列,最简单的消息队列模型包括3个角色:
- 消息队列:存储和管理消息,也被称作消息代理。
- 生产者:发送消息到消息队列。
- 消费者:从消息队列获取消息并处理消息。
Redis提供了三种不同的方式来实现消息队列:
- List结构:基于List结构模拟消息队列
- PubSub:基于点对点的消息模型
- Stream:比较完善的消息队列模型

这里只是演示一下Redis的消息队列,实际上要用消息队列是用MQ实现。
7.1 基于List结构模拟消息队列
Redis的list数据结构是一个双向链表,很容易就可以模拟出队列的效果。
我们只需要使用List结构的BRPOP或BLPOP来进行阻塞取出,LPUSH或RPUSH进行存入即可实现。
代码实现,就是将JVM的阻塞队列替换成Redis的List结构进行存取。
基于List的消息队列的优缺点:
- 优点:
- 利用Redis存储,不受JVM内存上限
- 基于Redis的持久化机制,数据安全性有保障
- 可以满足消息的有序性
- 缺点:
- 无法避免消息丢失
- 只支持单消费者
7.2 基于PubSub结构模拟消息队列
PubSub(发布订阅) 是Redis2.0版本引入的消息传递模型,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。 常用的命令:
subscribe channel [channel] 订阅一个或多个频道
publish channel msg 向一个频道发送消息
psubscribe pattern[pattern] 订阅与pattern格式匹配的所有频道
基于PubSub的消息队列优缺点:
- 优点:采用发布订阅模型,支持多生产,多消费
- 缺点:
- 不支持数据持久化
- 无法避免消息丢失
- 消息堆积有上限,超出时数据丢失
7.3 基于Stream的消息队列
Stream是Redis5.0引入的一种新的数据类型,可以实现一个功能非常完善的消息队列。
7.3.1 单消费者



Stream类型消息队列的XREAD命令特点:
- 消息可回溯
- 一个消息可以被多个消费者读取
- 可以阻塞读取
- 有消息漏读的风险
7.3.2 消费者组
创建消费者组:
bash
XGROUP create key groupName id [mkstream]
- key:队列名称
- groupName:消费者组名称
- ID:起始id标识,$代表队列中最后一个消息,0则代表队列中第一个消息。
- mkstream:队列不存在时自动创建队列
其他常见命令:
bash
# 消息确认
XACK key group ID [ID...]
# 查看pending-list
XPENDING key group [ [IDLE min-idle-time] start end count [consumer] ]
- IDLE min-idle-time:获取消息以后确认之前的时间,单位毫秒
- start:pendling-list里面最小的id,可以为-,即最小的
- end:pendling-list里面最大的id,可以为+,即最大的
- count:获取pendling的数量
- consumer:指定消费者的pendling-list

消费者监听基本思路:伪代码
Stream类型消息队列的XREADGROUP命令特点:
- 消息可回溯
- 可以多消费者争抢消息,加快消息速度
- 可以阻塞读取
- 没有消息漏读的风险
- 有消息确认机制,保证消息至少被消费一次
7.3.3 代码实现
-
创建一个Stream类型的消息队列,名称为:
stream.orders
objectivecXGROUP CREATE stream.orders g1 0 MKSTREAM
只需要创建一次即可,因为是一直保留在redis里面的。
-
修改之前的lua脚本,在确认有抢购资格后,直接向
stream.orders
中添加消息,内容包含:voucherId,userId,orderId
lua脚本修改
lua--1. 参数列表 -- 优惠券id local voucherId=ARGV[1] -- 用户id local userId=ARGV[2] -- 订单id local orderId=ARGV[3] --2. 数据key -- 库存key local stockKey='seckill:stock:' .. voucherId -- 订单key local orderKey='seckill:order:' .. voucherId --3. 脚本业务 -- 判断库存是否充足 get stockKey if(tonumber(redis.call('get',stockKey)) <= 0) then --库存不足,返回1 return 1 end -- 判断用户是否下单 if(redis.call('sismember',orderKey,userId) ==1) then -- 存在,说明是重复下单,返回2 return 2 end -- 扣库存 redis.call('incrby',stockKey,-1) -- 下单(保存用户) redis.call('sadd',orderKey,userId) -- 发送消息到队列中,XADD stream.orders * k1 v1 k2 v2 redis.call('XADD','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId) return 0
java代码修改:
java@Override public Result seckillVoucher(Long voucherId){ //获取用户 Long userId = UserHolder.getUser().getId(); //获取订单id long orderId = redisIdWorker.nextId("order"); //1.执行lua脚本 Long result=stringRedisTemplate.execute(SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(),String.valueOf(orderId) ); //2. 判断结果是否为0 int r = result.intValue(); if( r!= 0){ //不为0,代表没有购买资格 return Result.fail(r == 1 ? "库存不足":"不能重复下单"); } //获取代理对象 proxy =(IVoucherOrderService) AopContext.currentProxy(); //返回订单id return Result.ok(orderId); }
-
项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单。
- 执行任务的类
java@Component @Slf4j public class VoucherOrderHandler implements Runnable{ @Resource private StringRedisTemplate stringRedisTemplate; @Resource private RedissonClient redissonClient; //创建IVoucherOrderService代理对象 private IVoucherOrderService proxy; @Override public void run() { while(true){ try{ //1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.order > List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create("stream:orders", ReadOffset.lastConsumed()) ); //2判断消息是否获取成功 if(list == null || list.isEmpty()){ //如果失败,说明没有消息,继续下一次循环 continue; } //如果成功,则可以下单 //解析消息 MapRecord<String, Object, Object> record = list.get(0); Map<Object, Object> values = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true); //3创建订单 handleVoucherOrder(voucherOrder); //4.ack确认 sack stream.orders g1 id stringRedisTemplate.opsForStream().acknowledge("stream:orders","g1",record.getId()); }catch (Exception e){ log.error("订单异常",e); handlePendingList(); } } } private void handleVoucherOrder(VoucherOrder voucherOrder) throws InterruptedException { //1.获取用户 Long userId = voucherOrder.getUserId(); //2.创建锁对象 RLock lock = redissonClient.getLock("lock:order:" + userId); //获取锁(waitTime:获取锁的最大等待时长,给了这个参数那么获取锁不成功就会不断地去尝试,不给就是直接返回false) //leaseTime:锁失效的时间,即TTL boolean isLock = lock.tryLock(10L, TimeUnit.MINUTES); //判断锁是否获取成功 if(!isLock){ //不成功 log.info("不允许重复下单"); return; } try{ //创建订单 proxy.createVoucher(voucherOrder); }finally { lock.unlock(); } } private void handlePendingList(){ while(true){ try{ //1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.order 0 List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1), StreamOffset.create("streams:order", ReadOffset.from("0")) ); //2判断消息是否获取成功 if(list == null || list.isEmpty()){ //如果失败,说明pendling-list没有消息,结束循环 break; } //如果成功,则可以下单 //解析消息 MapRecord<String, Object, Object> record = list.get(0); Map<Object, Object> values = record.getValue(); VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true); //3创建订单 handleVoucherOrder(voucherOrder); //4.ack确认 sack stream.orders g1 id stringRedisTemplate.opsForStream().acknowledge("streams:order","g1",record.getId()); }catch (Exception e){ log.error("处理pending-list异常",e); try{ Thread.sleep(20); }catch (Exception ex){ ex.printStackTrace(); } } } } }