redis lua脚本解决高并发下秒杀场景
作者:光法V3
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依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
lua 脚本
1.减少库存,校验库存是否充足
2.库存数量回滚:
核心业务代码展示
1.加载lua脚本
private final static DefaultRedisScript<Long> deductRedisScript = new DefaultRedisScript(); private final static DefaultRedisScript<Long> 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<String, Long> 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<String> 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; }
以上就是redis lua脚本解决高并发下秒杀场景的详细内容,更多关于redis lua高并发秒杀的资料请关注脚本之家其它相关文章!