当前位置:网站首页>Redis(十一) - 异步优化秒杀
Redis(十一) - 异步优化秒杀
2022-08-02 05:13:00 【Super_Leng】
一、Redis优化秒杀
1. 压测秒杀业务
- 模拟1000请求,秒杀100张优惠券
- 1000个不同请求,则需要1000个不同token
编写测试方法生成token,存到token.txt 文件中:
@SpringBootTest
public class GenerateTokenTest {
@Resource
private UserServiceImpl userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void test() throws IOException {
List<User> userList = userService.query().list();
FileWriter fw = new FileWriter("e:/token.txt");
BufferedWriter bw = new BufferedWriter(fw);
for (User user : userList) {
String token = UUID.randomUUID().toString(true);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
bw.write(token);
bw.newLine(); // 换行符
bw.flush(); // 清除缓存
}
// 关闭
bw.close();
}
}
生成结果:

使用JMeter测试:



测试结果:
- 由结果可知,平均耗时1675毫秒,耗时较长,吞吐量362.2/s,吞吐量也不高
2. 优化秒杀思路
没优化之前的业务流程都是串行执行的,并且还会频繁和数据库交互,所以这就是耗时长,吞吐量不高的原因

判断秒杀库存、校验一人一单的业务都是查询操作,耗时较短,可以将该业务放到Redis中执行
减库存、创建订单业务会涉及到数据库的写操作,耗时长,可以单独开一个线程处理
主线程负责校验,校验成功后将订单id存到阻塞队列,并且将订单id返回给用户,用户可以接着执行其他业务
新启的线程异步读取阻塞队列中的信息,判断是否需要创建订单

由上面分析可知,需要提前将库存信息和优惠券订单信息缓存到Redis
库存信息可以用String类型存储,只要库存大于0就表示有库存;判断完之后还需要在Redis中预减库存;
优惠券订单信息需要用set集合存储,key是优惠券订单id,value是用户id,通过判断value是否存在就知道该用户是否下单过,保证了一人一单
为了确保判断秒杀库存、校验一人一单的原子性,所以需要使用Lua脚本

3. 改进秒杀业务
需求:
- 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
- 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
- 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
- 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
新增秒杀优惠券的同时,将优惠券信息保存到Redis中:
@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);
// 保存秒杀库存到Redis中
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

- Lua脚本
-- 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,2个..表示拼接字符串
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
-- tonumber是将字符串转为数字
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
改进的代码:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
// 创建阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
// 用线程池创建独立线程
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
// 项目启动后,就应该开启线程,异步从阻塞队列中获取信息
@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.创建订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
// 将代理对象作为全局变量,供所有线程使用
private IVoucherOrderService proxy;
private void handleVoucherOrder(VoucherOrder voucherOrder) {
// 1.获取用户,注意,这里是单独的线程,所以不能从主线程的ThreadLocal获取userId
Long userId = voucherOrder.getUserId();
// 2.创建锁对象
RLock redisLock = redissonClient.getLock("lock:order:" + userId);
// 3.尝试获取锁,这里加锁是兜底方案,可以不用再加锁,因为前面执行过lua脚本校验过一人一单
boolean isLock = redisLock.tryLock();
// 4.判断是否获得锁成功
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
log.error("不允许重复下单!");
return;
}
try {
// 注意:不能通过 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); 获取代理对象
// 因为AopContext.currentProxy();底层内部也有个ThreadLocal,但是此时的线程是新开启的线程,所以不能获取不到主线程中的代理对象
// 所以需要在主线程中先获取到代理对象,保存到全局变量供所有线程使用
proxy.createVoucherOrder(voucherOrder);
} finally {
// 释放锁
redisLock.unlock();
}
}
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
long orderId = redisIdWorker.nextId("order");
// 1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(), String.valueOf(orderId)
);
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
voucherOrder.setId(orderId);
// 2.4.用户id
voucherOrder.setUserId(userId);
// 2.5.代金券id
voucherOrder.setVoucherId(voucherId);
// 2.6.放入阻塞队列
orderTasks.add(voucherOrder);
// 3.在主线程中获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
// 4.返回订单id
return Result.ok(orderId);
}
@Transactional
@Override
public void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
log.error("用户已经购买过了");
return;
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
log.error("库存不足");
return;
}
// 7.创建订单
save(voucherOrder);
}
}
测试结果:
- 相比优化前,平均耗时缩短,吞吐量提高
4. 小结
秒杀业务的优化思路是什么?
- 先利用Redis完成库存余量、一人一单判断,完成抢单业务
- 再将下单业务放入阻塞队列,利用独立线程异步下单
基于阻塞队列的异步秒杀存在哪些问题?
- 内存限制问题(如果不对BlockingQueue做大小限制,则会有内存溢出问题)
- 数据安全问题(如果服务宕机,则内存的数据将会丢失)
边栏推荐
- 腾讯大咖分享 | 腾讯Alluxio(DOP)在金融场景的落地与优化实践
- Shuttle + Alluxio 加速内存Shuffle起飞
- 【合集- 行业解决方案】如何搭建高性能的数据加速与数据编排平台
- C language: Check for omissions and fill in vacancies (3)
- 本周大新闻|苹果MR已进行Pre-EVT测试,Quest 2涨价100美元
- How Navicat Connects to MySQL
- apisix-Getting Started
- How much does a test environment cost? Start with cost and efficiency
- 5年在职经验之谈:2年功能测试、3年自动化测试,从入门到不可自拔...
- 使用TinkerPop框架对GDB增删改查
猜你喜欢

How much does a test environment cost? Start with cost and efficiency

goroutine (coroutine) in go language

There are more and more talents in software testing. Why are people still reluctant to take the road of software testing?

国际顶会OSDI首度收录淘宝系统论文,端云协同智能获大会主旨演讲推荐
![[PSQL] 窗口函数、GROUPING运算符](/img/95/5c9dc06539330db907d22f84544370.png)
[PSQL] 窗口函数、GROUPING运算符

使用TinkerPop框架对GDB增删改查

国际顶会OSDI首度收录淘宝系统论文,端云协同智能获大会主旨演讲推荐

Automated operation and maintenance tools - ansible, overview, installation, module introduction

网安学习-内网渗透4

Linux CentOS8安装Redis6
随机推荐
Difference and analysis of CPU usage and load
回文串求解的进阶方法
Mysql implements optimistic locking
5年在职经验之谈:2年功能测试、3年自动化测试,从入门到不可自拔...
目标检测重要概念——IOU、感受野、空洞卷积、mAP
el-input can only input integers (including positive numbers, negative numbers, 0) or only integers (including positive numbers, negative numbers, 0) and decimals
golang环境详细安装、配置
There are more and more talents in software testing. Why are people still reluctant to take the road of software testing?
pytorch常用函数
【合集- 行业解决方案】如何搭建高性能的数据加速与数据编排平台
配合蓝牙打印的encoding-indexes.js文件内容:
H5 access payment process - WeChat payment & Alipay payment
Three methods of importing sql files in MySQL
ERROR 1045 (28000) Access denied for user 'root'@'localhost'Solution
非关系型数据库MongoDB的特点及安装
【OpenCV从入门到实践】图像处理技术[像素](全网最详细)
51单片机外设篇:ADC
Review: image saturation calculation formula and image signal-to-noise (PSNR) ratio calculation formula
How H5 realizes evoking APP
Contents of encoding-indexes.js file printed with Bluetooth: