@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1672531200;
/**
* 序列号位数
*/
private static final int COUNT_BITS = 32;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* ID自动生成器并返回
*
* @param keyPrefix 业务前缀
* @return
*/
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 自增长
long count = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + ":" + date);
//3 拼接并返回
return timeStamp << COUNT_BITS | count;
}
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2023, 1, 1, 0, 0, 0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println("second:" + second);
}
}
//秒杀券信息
{
"shopId": 1,
"title": "100元代金券",
"subTitle": "周一到周日均可使用",
"rules": "全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食",
"payValue": 8000,
"actualValue": 10000,
"type": 1,
"stock": 100,
"beginTime":"2023-04-18T15:40:00",
"endTime":"2023-04-18T23:40:00"
}
//VoucherOrderController类中
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
//IVoucherOrderService接口中声明方法
Result seckillVoucher(Long voucherId);
@Service
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);
//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
voucherOrder.setUserId(UserHolder.getUser().getId());
//6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7 返回订单id
return Result.ok(orderId);
}
}
在VoucherOrderServiceImpl类的seckillVoucher方法中修改操作数据库条件
//5 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") //set stock=stock-1
.eq("voucher_id",voucherId).eq("stock",voucher.getStock()) //where id=? and stock=?
.update();
改进,提高成功率
//5 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") //set stock=stock-1
.eq("voucher_id",voucherId).gt("stock",0) //where id=? and stock>0
.update();
这个实现过程比较复杂,包含spring事务失效、aop代理对象、synchronized锁等知识点,可以多看几遍。
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@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("库存不足!");
}
//synchronized悲观锁,给用户加锁
//要在事务外层加锁,因为要在事务提交之后释放锁,才能确保线程安全
Long userId = UserHolder.getUser().getId();
synchronized(userId.toString().intern()){
//使用代理对象的createVouterOrder方法才能开启事务
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVouterOrder(voucherId);
}
}
@Transactional
public Result createVouterOrder(Long voucherId) {
//A 新增一人一单业务
Long userId = UserHolder.getUser().getId();
//A1 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//A2 判断是否存在当前id订单
if(count>0){
//用户已购买过
return Result.fail("用户已购买过一次!");
}
//5 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") //set stock=stock-1
.eq("voucher_id", voucherId).gt("stock",0) //where id=? and stock=?
.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
voucherOrder.setUserId(userId);
//6.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7 返回订单id
return Result.ok(orderId);
}
}
在接口中声明createVouterOrder方法
Result createVouterOrder(Long voucherId);
在获取代理对象防止事务失效时,要在pom文件中增加一个依赖
org.aspectj
aspectjweaver
并在启动类HmDianPingApplication增加注解
@EnableAspectJAutoProxy(exposeProxy = true)
集群中,有多个JVM存在,每个JVM内部都维护着各自的锁,因此仍然有若干个线程能获取到不同JVM中的锁,这就是集群导致的线程安全问题。因此需要让多个JVM共用一把锁。
**Redis分布式锁原理:**基于setnx命令–>key存在的情况下,不更新value,而是返回nil
利用key是唯一的特性来加锁,比如一人一单业务,key名称精确到userId,那么同一个用户无论发多少次请求,能成功创建键值的只有一个,因为setnx命令,后面的请求在获取锁创建键值就会失败。
锁接口
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true表示获取锁成功 false表示获取失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unLock();
}
锁实现:
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
//锁名称(业务名)
private String name;
//锁前缀
private static final String KEY_PREFIX = "locks:";
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);
//自动拆箱会有空指针风险
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
在抢券业务中使用锁实现一人一单:
VoucherOrderServiceImpl类的seckillVoucher方法修改:
@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("库存不足!");
}
//synchronized悲观锁,给用户加锁
//要在事务外层加锁,因为要在事务提交之后释放锁,才能确保线程安全
Long userId = UserHolder.getUser().getId();
// synchronized(userId.toString().intern()){
//创建所对象
SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
//获取锁
boolean isLock = lock.tryLock(5);
//判断是否获取锁成功
if(!isLock){
//失败,返回错误信息
return Result.fail("不允许重复下单");
}
try {
//使用代理对象的createVouterOrder方法才能开启事务
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVouterOrder(voucherId);
} finally {
//释放锁
lock.unLock();
}
// }
}
把别的线程的锁删了。
解决方案: 获取锁时生成线程标识,在释放锁时判断线程标识是否是自己的,不一致可能是别人的锁,不释放。
修正:
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
//锁名称(业务名)
private String name;
//锁前缀
private static final String KEY_PREFIX = "locks:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
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);
//自动拆箱会有空指针风险
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);
}
}
}
判断锁和释放锁之间产生阻塞导致问题,因此这两个操作要变成原子性操作。
Redis事务是批处理,不能实现先判断锁一致再删除。并且Redis事务只能保证原子性,不能保证一致性。因此,使用Redis的Lua脚本来实现判断锁和释放锁操作的原子性。
修正:
利用lua的原子性特征,将判断和删除锁绑定为原子性操作。
Lua脚本:(放在resources目录下)
-- 这里的 KEY[1] 这就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 比较线程标示和锁中标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del',KEYS[1])
end
return 0
SimpleRedisLock类中unlock方法调用lua脚本
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
//锁名称(业务名)
private String name;
//锁前缀
private static final String KEY_PREFIX = "locks:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
//提前加载lua脚本
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
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);
//自动拆箱会有空指针风险
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
//调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}
1、配置POM依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2、配置Redisson
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){// RedissonClient是工厂类
//配置
Config config = new Config();
//添加redis地址,这里添加的单点地址,集群要使用config.useClusterServers()添加地址
config.useSingleServer().setAddress("redis://【虚拟机ip】:6379").setPassword("【redis密码】");
//创建Redisson对象
return Redisson.create(config);
}
}
3、使用Redisson分布式锁
// VoucherOrderServiceImpl类中注入RedissonClient对象
@Resource
private RedissonClient redissonClient;
// VoucherOrderServiceImpl类seckillVoucher方法中【创建锁,获取和释放锁语句修改】
// 创建锁对象
// SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
// 获取锁
// boolean isLock = lock.tryLock(1200);
// 释放锁
// lock.unlock();
//创建锁对象(可重入)
RLock lock = redissonClient.getLock("lock:order:" + userId);
//尝试获取锁,参数是:获取锁的最大等待时间(默认不等待),锁自动释放时间,时间单位
boolean isLock = lock.tryLock();
//释放锁
lock.unlock();
【利用信号量控制锁重试等待】:消息订阅+信号量机制,不是无休止重试,是有人释放锁之后再重试。
前14分钟
watchDog(看门狗)锁续约时间实现
14:00之后
连锁策略:不再有主从节点,都获取成功才能获取锁成功,有一个节点获取锁不成功就获取锁失败。
如果多个主节点保证锁的话,一个主节点宕机了,其它线程只能获得一个新主节点的锁,获取不到其它两个锁,还会获取失败
这里主要是防止主节点宕机后,其它线程获得新主节点的锁,引起线程安全问题。
可以将耗时较短的第一个线程操作放到 Redis 中,在 Redis 中处理对应的秒杀资格的判断。Redis 的性能是比 MySQL 要好的。此外,还需要引入【异步队列】记录相关的信息。
1、redis部分处理逻辑, Lua脚本封装操作保证原子性, redis这里选择的存储类型为set,因为key不能重复,而set恰好是无序不重复的
1.新增优惠券的业务时,把秒杀优惠券的库存信息保存到redis
// VoucherServiceImpl类中
//保存秒杀库存到redis
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());
2、编写lua脚本,按照下面的业务流程逻辑,在脚本中完成业务实现
-- 1.参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1 库存key key 是优惠的业务名称加优惠券id value 是优惠券的库存数
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单key key 也是拼接的业务名称加优惠权id 而value是用户id, 这是一个set集合,凡购买该优惠券的用户都会将其id存入集合中
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1 判断库存是否充足 get stockKey
if (tonumber(redis.call('get', stockKey)) <= 0) then --将get的value先转为数字类型才能判断比较
-- 3.2 库存不足,返回1
return 1
end
-- 3.3 判断用户是否下单 sismember orderKey userId命令,判断当前key集合中,是否存在该value;返回1存在,0不存在
if (redis.call('sismember', orderKey, userId) == 1) then
--3.4 存在说明是重复下单,返回2
return 2
end
-- 3.5 扣库存
redis.call('incrby', stockKey, -1)
-- 3.6 下单(保存用户)
redis.call('sadd', orderKey, userId)
return 0
3、VoucherOrderServiceImpl类中执行lua脚本,并判断,抢购成功的生成订单并存入阻塞队列
//注入脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
//提前加载lua脚本
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
//运行脚本,且判断不满足的请求直接返回提示信息
@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
if(result.intValue() != 0){
//2.1 不为0,没有购买资格
return Result.fail(result.intValue()==1?"库存不足":"不能重复下单");
}
//2.2 为0,有购买资格;把下单信息保存到阻塞队列
long orderId = redisIdWorker.nextId("order:");
//TODO: 保存阻塞队列
//3 返回订单id
return Result.ok(orderId);
}
4、如果抢购成功,将优惠券 id 和用户 id 封装后存入阻塞队列
4.1、创建BlockingQueue阻塞队列
BlockingQueue阻塞队列特点:当一个线程尝试从队列获取元素的时候,如果没有元素该线程阻塞,直到队列中有元素才会被唤醒并获取元素。
//阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
4.2、将满足条件的请求,生成订单,并把订单对象add到阻塞队列中
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
if (result.intValue() != 0) {
//2.1 不为0,没有购买资格
return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");
}
//2.2 为0,有购买资格;把下单信息保存到阻塞队列
//2.2.1 封装订单id,用户id,代金券id
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
//2.2.2 放入阻塞队列
orderTasks.add(voucherOrder);
// 提前获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
//3 返回订单id给客户端
return Result.ok(orderId);
}
5、开启线程任务,实现异步下单功能
5.1、首先创建一个线程池,再定义一个线程任务
【注意】线程任务需要在用户秒杀订单之前开始,用户一但开始秒杀,队列就会有新的订单,线程任务就应该立即取出订单信息,这里利用spring提供的注解,在类初始化完毕后立即执行线程任务。
//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
5.2、线程任务代码
//线程任务,内部类方式
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while(true){
//1 获取队列中订单信息
try {
VoucherOrder voucherOrder = orderTasks.take();
//2 创建订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常"+e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//1 获取用户id
Long userId = voucherOrder.getUserId();
//2 创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
//3 尝试获取锁,参数是:获取锁的最大等待时间(默认不等待),锁自动释放时间,时间单位
boolean isLock = lock.tryLock();
//判断是否获取锁成功
if(!isLock){
//失败,返回错误信息
log.error("不允许重复下单");
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
} finally {
//释放锁
lock.unlock();
}
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
//A 新增一人一单业务
Long userId = voucherOrder.getUserId();
//A1 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
//A2 判断是否存在当前id订单
if(count>0){
//用户已购买过
log.error("用户已经购买一次!");
return;
}
//5 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") //set stock=stock-1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0) //where id=? and stock=?
.update();
if(!success){
//扣减失败
log.error("库存不足!");
return;
}
//创建订单
save(voucherOrder);
}
由于前面的阻塞队列是基于JVM的内存实现,那么不可避免的两个大问题
①高并发海量访问,创建订单,队列很快就超出上限,造成内存溢出;
②JVM内存没有持久化机制,若服务出现重启或宕机,阻塞队列中的所有任务都会丢失。
MQ(消息队列)优点: MQ是JVM以外的服务,不受JVM内存限制,且MQ中的所有消息会做持久化,这样即使重启或宕机,数据不会丢失。消息投递给消费者后需要消费者确认,未确认消息会一直存在下一次继续投递,确保消息至少被消费一次。
P69使用测试类创建Jmeter压力测试用的tokens.txt文件,并将所有用户存到redis。
package com.hmdp;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.service.IUserService;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
/**
* @author xsy
* @version 1.0
*/
// P69创建 tokens.txt文件
@SpringBootTest
@AutoConfigureMockMvc
public class VoucherOrderControllerTest {
@Autowired
private IUserService userService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@SneakyThrows
@Test
void getTokens(){
FileOutputStream outputStream = new FileOutputStream(new File("D:\\JavaCode\\redis\\hm-dianping\\tokens.txt"));//tokens.txt文件存储路径
List<User> userList = userService.query().list();
userList.forEach(user -> {
String token = UUID.randomUUID().toString(true);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
new CopyOptions().
ignoreNullValue().
setFieldValueEditor((keyType, valueType) -> valueType.toString()));
String key = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(key, userMap);
try {
outputStream.write(token.getBytes(StandardCharsets.UTF_8));
outputStream.write("\n".getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
XGROUP CREATE stream.orders g1 0 MKSTREAM
-- 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 key 是优惠的业务名称加优惠券id value 是优惠券的库存数
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单key key 也是拼接的业务名称加优惠权id 而value是用户id, 这是一个set集合,凡购买该优惠券的用户都会将其id存入集合中
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1 判断库存是否充足 get stockKey
if (tonumber(redis.call('get', stockKey)) <= 0) then --将get的value先转为数字类型才能判断比较
-- 3.2 库存不足,返回1
return 1
end
-- 3.3 判断用户是否下单 sismember orderKey userId命令,判断当前key集合中,是否存在该value;返回1存在,0不存在
if (redis.call('sismember', orderKey, userId) == 1) then
--3.4 存在说明是重复下单,返回2
return 2
end
-- 3.5 扣库存
redis.call('incrby', stockKey, -1)
-- 3.6 下单(保存用户)
redis.call('sadd', orderKey, userId)
-- 3.7 发送消息到队列中:XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
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
if (result.intValue() != 0) {
//2.1 不为0,没有购买资格
return Result.fail(result.intValue() == 1 ? "库存不足" : "不能重复下单");
}
// 提前获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
//3 返回订单id给客户端
return Result.ok(orderId);
}
private class VoucherOrderHandler implements Runnable{
String queueName = "stream.orders";
@Override
public void run() {
while(true){
try {
//1 获取消息队列中订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 streams.orders >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
//2 判断消息是否获取成功
if(list == null || list.isEmpty()){
//2.1 如果获取失败,说明没有消息,继续下一次循环
continue;
}
// 解析消息中的订单信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//2.2 如果获取成功,可以下单创建订单
System.out.println("2-handleVoucherOrder(voucherOrder)");
handleVoucherOrder(voucherOrder);
//3 ACK确认 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
} catch (Exception e) {
handlePendingList();
log.error("处理订单异常"+e);
}
}
}
private void handlePendingList(){
while(true){
try {
//1 获取消息队列中订单信息 XREADGROUP GROUP g1 c1 COUNT 1 streams.orders 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queueName, ReadOffset.from("0"))
);
//2 判断消息是否获取成功
if(list == null || list.isEmpty()){
//2.1 如果获取失败,说明pending-list没有消息,结束循环
break;
}
// 解析消息中的订单信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//2.2 如果获取成功,可以下单创建订单
System.out.println("1-handleVoucherOrder(voucherOrder)");
handleVoucherOrder(voucherOrder);
//3 ACK确认 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
} catch (Exception e) {
log.error("处理pending-list订单异常"+e);
try {
Thread.sleep(20);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
}
}
}