一、Redis优化秒杀:异步秒杀
原先的步骤:
1、查询优惠券
2、判断秒杀库存;
3、查询订单;
4、校验一人一单;
5、减库存、创建订单;
这些 涉及到大量数据库的操作,所以在高并发 的情况下,性能并不是很好;
解决方案:通过redis,先将优惠券库存信息和订单信息存入redis,用lua脚本执行,保证原子性;将用户id用set集合存到redis中,对于同一张优惠券,如果userId存在,用户不能重复下单,以此判断用户是否有资格购买,然后异步开启独立线程执行,创建订单,放入阻塞队列,线程读取队列中的信息,完成秒杀操作,异步创建订单,与同步创建相比,在高并发情况下,大大提高响应时间。
二、基于redis完成异步秒杀资格判断
秒杀业务的优化思路?
1、先利用Redis完成库存余量、一人一单的判断,完成抢单业务
2、再将下单业务放到阻塞队列,利用独立线程异步下单;基于阻塞队列的异步秒杀存在的问题?
1、内存限制问题;如果服务宕机,会导致内存数据丢失,任务丢失;
2、数据安全问题;
/**
* 创建阻塞队列,1024 * 1024 指定队列大小,避免cpu过多消耗,将订单信息放入阻塞队列中
*/
private BlockingQueue<VoucherOrder> voucherOrderBlockingQueue = new ArrayBlockingQueue<>(1024 * 1024);
/**
* 异步线程,执行保存订单到数据库
*/
private static final ExecutorService VOUCHER_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
/**
* PostConstruct注解,什么时候执行?在用户秒杀执行之前去执行,这个任务在这个类初始化之后就来执行submit
*/
@PostConstruct
private void init() {
VOUCHER_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
/**
* 异步线程,从阻塞队列中取出订单信息,执行保存订单到数据库
*/
public class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while (true){
// 只要队列中有,就不断去取,不用担心陷入死循环,因为take()还有队列中有才会取,没有就不会拿
try {
VoucherOrder voucherOrder = voucherOrderBlockingQueue.take();
// 创建订单
createVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("订单异常信息",e);
}
}
}
}
/**
* 用于异步秒杀哦,创建订单,不会影响秒杀业务的执行;兜底方案
* @param voucherOrder
*/
private void createVoucherOrder(VoucherOrder voucherOrder) {
Long voucherId = voucherOrder.getVoucherId();
Long userId = voucherOrder.getUserId();
String name = "order:" + userId;
// 拿到锁对象锁
RLock clientLock = redissonClient.getLock(RedisConstants.LOCK + name);
// 不传参,失败了直接结束,默认值 ong time = 0L, TimeUnit unit 30s,超时30s之后直接是否
boolean tryLock = clientLock.tryLock();
if (!tryLock){
log.error("不能重复领取优惠券");
return;
}
try {
int count = this
.query()
.eq("user_id", userId)
.eq("voucher_id", voucherId).count();
if (count > 0) {
log.error("你已下过单");
return;
}
boolean sucess = iSeckillVoucherService.update()
// CAS 法 Compare and Switch**:比较修改。在版本号的基础上,
// 既然用version字段前后可以比较得出这条数据是否发生变化,那同样,
// 直接用stock库存本身来比较,stock前后是否发生了变化;
.setSql("stock = stock -1") // set stock = stock -1
.eq("voucher_id", voucherId).gt("stock", 0)
// 乐观锁的缺点:** 成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,
// 其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
// 所以这里这样判断stock > 0即可
.update();
if (!sucess) {
log.error("库存不足");
return;
}
// 6、将数据存入优惠券订单表
save(voucherOrder);
} finally {
// 判断线程是不是当前线程
// 2、最后一定要释放锁
clientLock.unlock();
}
}
三、Redis消息队列实现异步秒杀
消息队列:包含
1、消息队列:存储和管理消息,也被称为消息代理M(Message Broker)
2、生产者:发送消息到消息队列;
3、消费者:从消息队列获取消息并处理消息;
消息队列与阻塞队列的区别:
1、消息队列是在JVM以外的独立服务,不受jvm内存的限制;解决了内存限制问题
2、消息队列的数据要做持久化,如果服务宕机了,数据也不会丢失;而且,消息队列要消费者做消息的确认,确保消息至少被消费一次;
3、市面上的rabbitMQ、kafka等等,但是redis可以利用list结构模拟消息队列;
四、基于List结构模拟消息队列
五、基于Pubsub的消息队列
发布订阅:是redis2.0版本引入的消息传递模型,消费者可以订阅一个或者多个频道,向对应的频道发送消息之后,所有订阅者都能收到消息;
六、基于Stream的消息队列
七、Stream的消费者组
消费者组:将多个消费者划分到一个小组中,监听同一个队列;具备以下特点:
1、消息分流:队列中的消息分流给组内的不同消费者,而不是重复性消费,避免消息堆积,加快消息处理速度;
2、消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,会从标志之后读取消息,确保每一个消息都会被消费;
3、消息确认:消费者获取消息之后,消息处于pending(待处理)状态,并存入一个penging-list列表。处理完成之后需要通过XACK来确认消息,标记消息为已处理,并从pending-list中移除;
八、基于Stream消息队列实现异步秒杀
/**
* 新的玩法:异步线程,从消息队列中取出订单信息,执行保存订单到数据库
*/
public class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
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()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
handlePendingList();
}
}
}
private void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.from("0"))
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有异常消息,结束循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户
Long userId = UserHolder.getUser().getId();
// 执行seckill.lua脚本
Long result = stringRedisTemplate.execute(SECKILL_LUA, Collections.emptyList(), voucherId.toString(), userId.toString());
// 拆箱
int i = result.intValue();
// 判断是否为0,不是0,为1的话说明是库存不足,为2说明重复下单
if (i != 0){
return Result.fail(i == 1 ? "库存不足" : "不能重复下单");
}
// todo 将创建的订单信息保存到消息队列
long orderId = redisIdWorker.generateOnlyId("order");
// 返回订单id
return Result.ok(orderId);
}