Redis lua脚本解决抢购秒杀场景
介绍
秒杀抢购可以说是在分布式环境下⼀个⾮常经典的案例,⾥边有很多痛点:
1.⾼并发: 时间极短、瞬间⽤户量⼤,⼀瞬间的⾼QPS把系统或数据库直接打死,响应失败,导致与这个系统耦合的系统也GG
目前秒杀的实现方案主要有两种:
2.超卖: 你只有⼀百件商品,由于是⾼并发的问题,导致超卖的情况
目前秒杀的实现方案主要有两种:
1.用redis 将抢购信息进行存储。然后再慢慢消费。 同时,服务器给与用户快速响应。
2.用mq实现,比如RabbitMQ,服务器将请求过来的数据先让RabbitMQ存起来,然后再慢慢消费掉。
也可以结合redis与mq的方式,通过redis控制剩余库存,达到快速响应,将满足条件的购买的订单先让RabbitMQ存起来,后续在慢慢消化。
整体流程:
1.服务器接收到了大量用户请求过来(1s 2000个请求)。比如传了用户信息,产品信息,和购买数量信息。此时 服务器采用redis 的lua 脚本 去调用redis 中间件。lua 脚本的逻辑是减库存,校验库存是否足够。然后迅速给与服务器反馈(库存是否够,够返回 1 ,不够返回 0)。
2.服务器迅速给与用户的请求反馈。提示抢购成功.或者抢购失败
3.抢购成功,将订单信息放入MQ,其余线程接受到MQ的信息后,将订单信息存入DB中
4.后面客户就可以查询 mysql 的订单信息了。
代码展示
架构采用springboot+redis+mysql+myBatis.
数据库
CREATE TABLE `tb_product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'id',
`price` decimal(65,18) NOT NULL DEFAULT '0',
`available_qty` bigint NOT NULL DEFAULT '0' COMMENT '发行数量',
`title` varchar(1024) NOT NULL DEFAULT '',
`end_time` bigint NOT NULL DEFAULT '0',
`start_time` bigint NOT NULL DEFAULT '0',
`created` bigint NOT NULL DEFAULT '0',
`updated` bigint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
pom依赖:
org.springframework.boot
spring-boot-starter-data-redis
lua 脚本:
1.减少库存,校验库存是否充足
2.库存数量回滚:
核心业务代码展示
1.加载lua脚本
private final static DefaultRedisScript deductRedisScript = new DefaultRedisScript();
private final static DefaultRedisScript increaseRedisScript = new DefaultRedisScript();
//加载lua脚本
@PostConstruct
void init() {
//加载削减库存lua脚本
deductRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedDeductInventory.lua")));
deductRedisScript.setResultType(Long.class);
//加载库存回滚lua脚本
increaseRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedIncreaseInventory.lua")));
increaseRedisScript.setResultType(Long.class);
}
2.添加库存到redis
**注意点:**在使用redis集群时,lua脚本中存在多个key时,可以通过hash tag
这个方法将不同key的值落在同一个槽位上,hash tag 是通过{}
这对括号括起来的字符串,如果下列中{fixed:" + data.getProductId() + "} 作为tag,确保同一个产品的信息都在同一个槽位。
@Resource(name = "fixedCacheRedisTemplate")
private RedisTemplate fixedCacheRedisTemplate;
public void ProductToOngoing(Product data, Long time) {
//设置数量
long number = data.getAvailableQty();
fixedCacheRedisTemplate.opsForHash().putIfAbsent("{fixed:" + data.getProductId() + "}-residue_stock_" + data.getRecordId(),
"{fixed:" + data.getProductId() + "}-residueStock" , number);
String statusKey = "fixed_product_sold_status_"+ data.getRecordId();
long timeout = data.getEndTime() - data.getStartTime();
//添加产品出售状态
fixedCacheRedisTemplate.opsForValue().set(statusKey, 1L, data.getEndTime() - data.getStartTime(), TimeUnit.MILLISECONDS);
}
3.下单&库存校验
//检查库存
public boolean checkFixedOrderQty(Long userId, Long productId, Long quantity, Long overTime) {
Boolean pendingOrder = false;
String userKey = "";
try {
//校验是否开始
String statusKey = "fixed_product_sold_status_" + productId;
Long fixedStartStatus = fixedCacheRedisTemplate.opsForValue().get(statusKey);
if (fixedStartStatus == null || fixedStartStatus != 1L) {
//报错返回,商品未开售
throw new WebException(ResultCode.SALE_HAS_NOT_START);
}
//检查库存数量
Long number = deductInventory(productId, quantity);
if (number != 1L) {
log.warn("availbale num is null:{} {}", productId, number);
throw new WebException(ResultCode.AVAILABLE_AMOUNT_INSUFFICIENT);
}
return true;
} catch (Exception e) {
log.warn("checkFixedOrderQty error:{}", e.getMessage(), e);
throw e;
}
}
//下单
public void createOrder(Long userId, Long productId, BigDecimal price, Long quantity){
boolean check = checkFixedOrderQty(userId, productId, quantity);
try {
if (check) {
//添加MQ等待下单,后续收到推送的线程保存靠DB中
CreateCoinOrderData data = new CreateCoinOrderData();
data.setUserId(userId);
data.setProductId(productId);
data.setPrice(price);
data.setQuantity(quantity);
rabbitmqProducer.sendMessage(1, JSONObject.toJSONString(data));
}
} catch (Exception e) {
//发生异常,库存需要回滚
increaseInventory(recordId, quantity, 1L);
throw e;
}
}
//库存回填
public Long increaseInventory(Long productId, Long num) {
try {
// 构建keys信息,代表hash值中所需要的key信息
List keys = Arrays.asList("{fixed:" + productId + "}-residue_stock_"+ recordId, "{fixed:" + productId + "}-residueStock");
// 执行脚本
Object result = fixedCacheRedisTemplate.execute(increaseRedisScript, keys, num);
log.info("increaseInventory productId :{} num:{} result:{}", productId, num, result);
return (Long) result;
} catch (Exception e) {
log.warn("increaseInventory error productId:{} num:{}", productId, num);
}
return 0L;
}