基于代码实操SpringBoot、Redis、LUA秒杀系统

发布时间:2025-05-15 16:09:44 作者:益华网络 来源:undefined 浏览量(1) 点赞(3)
摘要:前言 那些吧redis基本的东西学的差不多了,却没有做过什么具体的项目实践的,可以看看这篇文章做一个项目来巩固知识。 相关需求&说明 一般来说秒杀系统的功能不会很多,有: 制定秒杀计划。在某天几点开始,售卖什么商品,准备卖多少个,持续多久。 展

前言

那些吧redis基本的东西学的差不多了,却没有做过什么具体的项目实践的,可以看看这篇文章做一个项目来巩固知识。

相关需求&说明

一般来说秒杀系统的功能不会很多,有:

制定秒杀计划。在某天几点开始,售卖什么商品,准备卖多少个,持续多久。 展示秒杀计划列表。一般都是显示当天的,8点卖一些,10点卖一些这种。 商品详情页。 下单购买。 等等

本文主要目的还是用代码实现一下防止商品超卖的功能,所以像制定秒杀计划,展示商品等功能就不着重写了。

还有电商的商品主要是SPU(例如iPhone 12,iPhone 11就是两个SPU)及SKU(例如iPhone 12 64G 白色,iPhone 12 128G 黑色就是两个SKU)的处理,展示的是SPU,购买扣库存的是SKU,本文为了方便,就直接用product来替代了。

下单购买还会有一些前置条件,比如要经过风控系统,确认你是不是黄牛;营销系统,有没有相关的优惠券,虚拟货币之类的。

下单完成还要走库管、物流,还有积分之类的,本文就不涉及了。 本文不涉及数据库,一切都在Redis上操作,不过还是想说一下数据库与缓存数据一致性的问题。

如果我们的系统并发不高,数据库撑得住,则直接操作数据库即可,为防止超卖,可以采用:

悲观锁

select * from SKU表 where sku_id=1 for update;

乐观锁

update SKU表 set stock=stock-1 where sku_id=1 and update_version=旧版本号; 

果并发高一些,例如商品详情页一般并发最高,为了减少数据库的压力,都会使用Redis等缓存,为了保证数据库与Redis的一致性,多是采用“修改后删除”方案。 但是这个方案在更高并发情况下,如C10K、C10M等,在修改数据库并删除Redis内容的一瞬间,大量查询并发会传导至数据库,产生异常。 这种情况,SPU详情这种接口就坚决不能与数据库连接起来。 步骤应该是:

B端管理系统操作数据库(这个并发不会高)。 数据入库后,发送消息给MQ。 相关处理程序在接收到订阅的MQ的Topic后,从数据库取出信息,放入Redis。 相关服务接口只从Redis取数据。

代码实现

在实际项目中,建议将ToC端的秒杀产品相关接口组合为一个微服务,product-server。售卖接口组合为一个微服务,order-server。可以参考之前的Spring Cloud系列文章进行编码,本文就简单使用了一个Spring Boot工程。

秒杀计划实体类

省略get/set

public class SecKillPlanEntity implements Serializable {     private static final long serialVersionUID = 8866797803960607461L;     /**      * id      */     private Long id;     /**      * 商品id      */     private Long productId;     /**      * 商品名称      */     private String productName;     /**      * 价格 单位:分      */     private Long price;     /**      * 划线价 单位:分      */     private Long linePrice;     /**      * 库存数      */     private Long stock;     /**      * 一个用户只买一件商品标识 0否1是      */     private int buyOneFlag;     /**      * 计划状态 0未提交,1已提交      */     private int planStatus;     /**      * 开始时间      */     private Date startTime;     /**      * 结束时间      */     private Date endTime;     /**      * 创建时间      */     private Date createTime; }

说明:

正如前文所说,秒杀的商品应该展示的是SPU,售卖扣库存的是SKU,本文为了方便,只用product来替代。

用户购买秒杀商品,有两种方式:

一个用户只允许购买一件。 一个用户可以多次购买多件。

所以本类使用buyOneFlag做标识。

planStatus代表本次秒杀是否真正执行。0不展示给C端,不进行售卖;1展示给C端,进行售卖。

添加秒杀计划&查询秒杀计划

@RestController public class ProductController {     @Resource     private RedisTemplate<String, String> redisTemplate;     // 随机生成秒杀计划设置到Redis中     @GetMapping("/addSecKillPlan")     @ResponseBody     public DefaultResult<List<SecKillPlanEntity>> addSecKillPlan(@RequestParam("saledate") String saleDate) {         DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");         Random rand = new Random();         Gson gson = new Gson();         List<SecKillPlanEntity> list = Lists.newArrayList();         for (int i = 0; i < 10; i++) {             long productId = rand.nextInt(100) + 1;             long price = rand.nextInt(100) + 1;             long stock = rand.nextInt(100) + 1;             String saleStartTime = " 10:00:00";             String saleEndTime = " 12:00:00";             int buyOneFlag = 0;             if (i > 4) {                 saleStartTime = " 14:00:00";                 saleEndTime = " 16:00:00";                 buyOneFlag = 1;             }             SecKillPlanEntity entity = new SecKillPlanEntity();             entity.setId(i + 1L);             entity.setProductId(productId);             entity.setProductName("商品" + productId);             entity.setBuyOneFlag(buyOneFlag);             entity.setLinePrice(999999L);             entity.setPlanStatus(1);             entity.setPrice(price * 100);             entity.setStock(stock);             entity.setEndTime(Date                     .from(LocalDateTime.parse(saleDate + saleEndTime, dtf).atZone(ZoneId.systemDefault()).toInstant()));             entity.setStartTime(Date.from(                     LocalDateTime.parse(saleDate + saleStartTime, dtf).atZone(ZoneId.systemDefault()).toInstant()));             entity.setCreateTime(new Date());             // 商品详情写入Redis             ValueOperations<String, String> setProduct = redisTemplate.opsForValue();             setProduct.set("product_" + productId, gson.toJson(entity));             // 写入库存             if (buyOneFlag == 1) {                 // 一个用户只买一件商品                 // 商品购买用户Set                 redisTemplate.opsForSet().add("product_buyers_" + productId, "");                 // 商品库存                 for (int j = 0; j < stock; j++) {                     redisTemplate.opsForList().leftPush("product_one_stock_" + productId, "1");                 }             } else {                 // 用户可买多个                 redisTemplate.opsForValue().set("product_stock_" + productId, stock + "");             }             list.add(entity);             System.out.println(gson.toJson(entity));         }         redisTemplate.opsForValue().set("seckill_plan_" + saleDate, gson.toJson(list));         return DefaultResult.success(list);     }     @GetMapping("/findSecKillPlanByDate")     @ResponseBody     public DefaultResult<List<SecKillPlanEntity>> findSecKillPlanByDate(@RequestParam("saledate") String saleDate) {         Gson gson = new Gson();         String planJson = redisTemplate.opsForValue().get("seckill_plan_" + saleDate);         List<SecKillPlanEntity> list = gson.fromJson(planJson, new TypeToken<List<SecKillPlanEntity>>() {         }.getType());         // 设置新的库存         for (SecKillPlanEntity entity : list) {             if (entity.getBuyOneFlag() == 1) {                 long newStock = redisTemplate.opsForList().size("product_one_stock_" + entity.getProductId());                 entity.setStock(newStock);             } else {                 long newStock = Long                         .parseLong(redisTemplate.opsForValue().get("product_stock_" + entity.getProductId()));                 entity.setStock(newStock);             }         }         return DefaultResult.success(list);     } }

 说明:

addSecKillPlan就是随机生成10个售卖计划,有仅售一件的,也有售多件的。并将相关数据压入Redis。 seckill_plan_日期,代表某日的所有秒杀计划,列表展示用。 product_商品ID,代表某商品信息,详情页使用。 product_one_stock_商品ID,代表仅售一件商品的库存数,值是List,有多少库存,就往里面push多少个“1”。 product_buyers_商品ID,代表仅售一件商品的购买者,已购买过的用户不允许再买。 product_stock_商品ID,代表可售多件商品的库存数,值是库存数。

findSecKillPlanByDate,展示某日秒杀售卖计划。库存数从库存相关的两个KEY取。

LUA脚本

仅售一件buyone.lua:

--商品库存Key product_one_stock_XXX local stockKey = KEYS[1] --商品购买用户记录Key product_buyers_XXX local buyersKey = KEYS[2] --用户ID local uid = KEYS[3] --校验用户是否已经购买 local result=redis.call("sadd" , buyersKey , uid ) if(tonumber(result)==1) then      --没有购买过,可以购买     local stock=redis.call("lpop" , stockKey )     --除了nil和false,其他值都是真(包括0)     if(stock)     then          --有库存         return 1     else         --没有库存         return -1     end else     --已经购买过     return -3 end

 可售多件buymore.lua:

--商品Key local key = KEYS[1] --购买数 local val = ARGV[1] --现有总库存 local stock = redis.call("GET", key) if (tonumber(stock)<=0)  then     --没有库存     return -1 else     --获取扣减后的总库存=总库存-购买数     local decrstock=redis.call("DECRBY", key, val)     if(tonumber(decrstock)>=0)     then         --扣减购买数后没有超卖,返回现库存         return decrstock     else         --超卖了,把扣减的再加回去         redis.call("INCRBY", key, val)         return -2     end end

说明:

1、仅售一件。先把购买者的ID用命令“sadd”进product_buyers_商品ID,如果返回1,代表此用户之前没有购买过,否则返回-3,已经购买过。

在从product_one_stock_商品ID中lpop出数值,如果还有库存,必会返回1,有库存,否则就是nil,无库存。

2.、可售多件。之前讲过,不再描述。 将两个lua文件,放在Spring Boot工程的resources目录下。

售卖接口

@RestController public class OrderController {     @Resource     private RedisTemplate<String, String> redisTemplate;     @GetMapping("/addOrder")     @ResponseBody     public DefaultResult<Void> addOrder(@RequestParam("uid") long userId, @RequestParam("pid") long productId,             @RequestParam("quantity") int quantity) {         Gson gson = new Gson();         String productJson = redisTemplate.opsForValue().get("product_" + productId);         SecKillPlanEntity entity = gson.fromJson(productJson, SecKillPlanEntity.class);         //TODO 要校验售卖计划是否已提交,是否到了售卖时间         long code = 0;         if (entity.getBuyOneFlag() == 1) {             // 用户只买一件             code = this.buyOne("product_one_stock_" + productId, "product_buyers_" + productId, userId);         } else {             // 用户买多件             code = this.buyMore("product_stock_" + productId, quantity);         }         DefaultResult<Void> result = DefaultResult.success(null);         // 错误代码的处理应该使用ENUM,本文就节省了         if (code < 0) {             result.setCode(code);             if (code == -1) {                 result.setMsg("没有库存");             } else if (code == -2) {                 result.setMsg("库存不足");             } else if (code == -3) {                 result.setMsg("已经购买过");             }         }         return result;     }     private Long buyOne(String stockKey, String buysKey, long userId) {         DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();         defaultRedisScript.setResultType(Long.class);         defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buyone.lua")));         // "{pre}:"         List<String> keys = Lists.newArrayList(stockKey, buysKey, userId + "");         Long result = redisTemplate.execute(defaultRedisScript, keys, "");         return result;     }     private Long buyMore(String stockKey, int quantity) {         DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();         defaultRedisScript.setResultType(Long.class);         defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buymore.lua")));         List<String> keys = Lists.newArrayList(stockKey);         Long result = redisTemplate.execute(defaultRedisScript, keys, quantity+"");         return result;     } }

说明: 1、主要看buyOne、buyMore两个私有方法,里面写的是如何使用RedisTemplate执行lua脚本。

另外我看有资料说如果使用的是Redis集群,则会报错,因为我没有Redis的集群环境,所以也没法测试,大家有环境的可以试一试。

2、addOrder有一些代码为了节省时间,就写得很low了,比如一些校验没有加,错误码应该使用ENUM等。 测试用例:

A用户购买仅售一件商品1,成功。 A用户再购买仅售一件商品1,失败。 N用户购买仅售一件商品1,库存不足。 A用户购买可售多件商品2,成功。 A用户购买可售多件商品2,库存不足。

二维码

扫一扫,关注我们

声明:本文由【益华网络】编辑上传发布,转载此文章须经作者同意,并请附上出处【益华网络】及本页链接。如内容、图片有任何版权问题,请联系我们进行处理。

感兴趣吗?

欢迎联系我们,我们愿意为您解答任何有关网站疑难问题!

您身边的【网站建设专家】

搜索千万次不如咨询1次

主营项目:网站建设,手机网站,响应式网站,SEO优化,小程序开发,公众号系统,软件开发等

立即咨询 15368564009
在线客服
嘿,我来帮您!