问题场景
比如一个秒杀业务,我们需要判断用户是否有优惠券、是否已经参与过秒杀、库存是否足够、扣减库存、插入订单号业务。上述每一步都需要操作DB,这样的接口性能一定跟不上。
解决办法原理:
添加库存,String类型即可
用户获取优惠券时:添加一个set集合,用于保存所有的优惠券。
lua代码执行:判断用户是否有优惠券、是否参与秒杀、库存是否足够由set集合负责,扣件库存由redis的阻塞队列操作。
开启线程任务,不断从阻塞队列获取信息,实现真实DB下单功能
解决办法实操
获取优惠券代码
代码语言:javascript复制@Test
public void 添加库存() {
int activityId = 10086;
int activityAmount = 1000;
redisTemplate.opsForValue().set("秒杀业务:库存", activityAmount);
System.out.println("添加成功!");
}
代码语言:javascript复制@Test
public void 添加用户的秒杀资格() {
Integer userAccount = 740969606;
String redisKey = "秒杀业务:优惠券用户列表";
// 判断用户是否添加过优惠券
Boolean isMember = redisTemplate.opsForSet().isMember(redisKey, userAccount);
if (Boolean.FALSE.equals(isMember)) {
// 添加用户信息进入列表
Long add = redisTemplate.opsForSet().add(redisKey, userAccount);
System.out.println("添加成功");
} else {
System.out.println("用户已经拥有资格");
}
}
校验秒杀资格lua脚本seckill.lua
代码语言:javascript复制local userAccount = ARGV[1]
local key1 = "秒杀业务:库存"
local key2 = "秒杀业务:优惠券用户列表"
local key3 = "秒杀业务:用户下单列表"
-- 校验库存是否足够
local kucun = redis.call('get',key1)
if tonumber(kucun) > 0 then
-- 判断用户是否有购买资格
if redis.call('sismember',key2,userAccount) == 1 then
-- 判断用户是否下过单
if redis.call('sismember',key3,userAccount) == 0 then
-- 扣除库存
redis.call('incrby',key1,-1)
redis.call('sadd',key3,userAccount)
return 1
else
return 0 -- 下单失败:用户不可重复下单
end
else
return -2 -- 用户没有资格
end
else
return -1 -- 库存不足
end
异步线程 真实下单 (下面代码不得使用Junit测试)
定义线程池threadPools、阻塞队列绑定线程池与调用orders、初始化lua脚本killLuaScript、全局代理对象currentProxy(避免线程池产生子线程无法获取Spring管理的对象)
代码语言:javascript复制@Autowired
RedisTemplate redisTemplate;
private Object currentProxy;
public static final DefaultRedisScript killLuaScript;
static {
killLuaScript = new DefaultRedisScript();
killLuaScript.setLocation(new ClassPathResource("seckill.lua")); // 指定脚本文件路径
killLuaScript.setResultType(Long.class); // 指定脚本返回值类型
}
// 创建一个阻塞队列
private BlockingQueue<HashMap> orders = new ArrayBlockingQueue<>(1024 * 1024);
// 创建线程池
private static final ExecutorService threadPools = Executors.newSingleThreadExecutor();
// 交由Spring管理
@PostConstruct
private void init() {
threadPools.submit(new DIYHandle());
}
private class DIYHandle implements Runnable {
@Override
public void run() {
while (true) {
HashMap take = null;
try {
take = orders.take();
// 从全局变量中获取代理对象。使用Spring代理对象调用真实下单业务保证事务!我这个测试类叫RedisTemplateTests
UserService xxService = (UserService) currentProxy;
xxService.doRealOrder(take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
// 处理订单的类
@Transactional
public void doRealOrder(HashMap hashMap) {
System.out.println("真实下单业务执行了:" hashMap.toString());
}
@Override
public String seckill() {
TimeInterval timer = DateUtil.timer();
Integer userAccount = 740969606;
// 调用Lua,校验用户下单资格
Object executeRes = redisTemplate.opsForValue().getOperations().execute(killLuaScript, Collections.emptyList(), userAccount);
if ((Long) executeRes == 1L) {
// 将用户下单信息保存到阻塞队列中,方便处理数据
HashMap<String, Object> hashMap = new HashMap();
hashMap.put("orderId", UUID.randomUUID());
boolean add = orders.add(hashMap);
// 由于子线程不受主线程的事务控制,我们得手动获取为全局对象,以便于新开的子线程受到Spring事务的控制。Junit无法获取代理对象,下面会报错。真实Service调用没问题,必须删除下一行的注解
//currentProxy = AopContext.currentProxy();
System.out.println("下单成功!");
} else {
System.out.println("下单失败!");
}
System.out.println("消耗了:" timer.interval());
return timer.interval() "";
}
测试
1000个线程不停请求,吞吐量每秒10000,平均延迟100ms
Power By Mac M1 Pro
特殊说明: 以上文章,均是我实际操作,写出来的笔记资料,不会盗用别人文章!烦请各位,请勿直接盗用!转载记得标注来源!