那些吧redis基本的基于东西学的差不多了,却没有做过什么具体的代码项目实践的,可以看看这篇文章做一个项目来巩固知识。实操
一般来说秒杀系统的秒杀功能不会很多,有:
本文主要目的还是用代码实现一下防止商品超卖的功能,所以像制定秒杀计划,展示商品等功能就不着重写了。
还有电商的商品主要是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详情这种接口就坚决不能与数据库连接起来。 步骤应该是:
在实际项目中,建议将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);
- }
- }
说明:
findSecKillPlanByDate,展示某日秒杀售卖计划。库存数从库存相关的两个KEY取。
仅售一件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,已经购买过。
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等。 测试用例:
责任编辑:姜华 来源: 今日头条 SpringBootRedisLUA
(责任编辑:探索)
北京汽车(01958.HK)年度净利跌59.4% 每股收益为人民币0.24元