麒麟v10 上部署 TiDB v5.1.2 生产环境优化实践
687
2023-05-31
基于代码实操SpringBoot、Redis、LUA秒杀系统
前言
那些吧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> addSecKillPlan(@RequestParam("saledate") String saleDate) { DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); Random rand = new Random(); Gson gson = new Gson(); List
> findSecKillPlanByDate(@RequestParam("saledate") String saleDate) { Gson gson = new Gson(); String planJson = redisTemplate.opsForValue().get("seckill_plan_" + saleDate); List
>() { }.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
说明: 1、主要看buyOne、buyMore两个私有方法,里面写的是如何使用RedisTemplate执行lua脚本。
另外我看有资料说如果使用的是Redis集群,则会报错,因为我没有Redis的集群环境,所以也没法测试,大家有环境的可以试一试。
2、addOrder有一些代码为了节省时间,就写得很low了,比如一些校验没有加,错误码应该使用ENUM等。 测试用例:
A用户购买仅售一件商品1,成功。A用户再购买仅售一件商品1,失败。N用户购买仅售一件商品1,库存不足。A用户购买可售多件商品2,成功。A用户购买可售多件商品2,库存不足。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。