促销系统之秒杀功能模块


订单所涉及到的后台系统包括系统模块,网关模块,调度器模块,商品模块,促销模块,订单模块
库存系统(无-商品配额)、物流系统(无-到店取货)、风控系统(无)等。
订单业务的流转主要依靠完善的后台系统

促销系统

我负责的是秒杀模块和网关认证授权功能,秒杀模块其实是促销模块的一个子功能,由于我们的项目老开发人员比较急,为了优先实现功能,多个功能模块只是多模块开发,模块之间调用需要通过httpClient,并没有引入RPC框架;而且各模块的数据库的表也比较混乱没有分库,功能逻辑耦合度也比较大,而且并发能力也比较弱只能达到几百,我们做秒杀功能模块其实是为了后期把服务都迁移到新框架中。
我这次负责的是促销系统的秒杀功能,因为要抗击高并发,所以单独创建独立微服务,独立数据库。大佬把旧项目各个模块的数据库也进行了分库,然后做了微服务适配。我创建秒杀模块创建成独立服务,引入微服务网关进行认证和授权。我对系统安全方面和应对高并发方面比较了解。

http://www.woshipm.com/pd/916015.html

  1. 促销系统主要包括:活动管理和促销类型管理
    活动信息:活动名称,活动时间,活动规则,活动商品
    活动状态:未开始的活动:随意编辑或删除活动
    开始的活动:有终止活动操作,一般不允许修改相关的商品
    已结束的活动:不可编辑
  2. 促销类型:满减、满赠、满折、加价购、特价、套餐、预售、秒杀等。

风控系统比较简单,是通过设定业务规则,限定一个用户只能参加一个活动,而且只能抢购有限数量的商品

在这里插入图片描述

普通订单流程-区别于秒杀订单流程

在这里插入图片描述

前端订单系统

  1. 订单信息
    用户信息:联通手机号
    商品信息:规格,价格,数量
    活动信息,优惠金额
    支付信息:订单总金额,实付金额,优惠金额,支付单号,订单号
  2. 订单状态
    待付款,付款成功,退款中,退款失败,交易成功(),交易关闭(取消订单)

后台订单系统

后台订单系统和前端订单系统展示的信息相对应,包括订单列表以及订单详情的展示。

  1. 订单列表:查询所有用户所有的下单记录,主要是一些核心信息-订单编号,下单时间,下单用户,商品信息,实付金额,订单状态,维权状态
  2. 订单详情:点击某个订单查他的详情,分为三部分-订单信息,支付信息,门店信息
    订单信息:商品名称、规格、ID,商品单价、购买数量、实付金额
    支付信息: 商品总额、运费、优惠金额、实付金额、支付时间、支付单号、交易单号
    门店信息:门店名称,门店地址,门店联系电话

秒杀订单模块需要解决的问题

  1. 高并发引起的缓存雪崩,缓存击穿,缓存穿透
  2. 超卖,商家预估卖100个可以赚点还可以营销,结果多卖出200个,不发货用户投诉,平台封店,发货就血亏,怎么办。
  3. 恶意请求。验证码,后台逻辑效验等
  4. 链接暴露。在秒杀前置灰,加密url
  5. 把其他服务打挂。每秒上万请求直接打到数据库把库打挂,如果秒杀服务还涉及到其他业务,并且没做降级、限流、熔断啥的,会把其他服务一起打挂

针对秒杀活动对症下药

服务单一原则

我们系统是微服务架构,分布式部署,我们可以将秒杀也单独做一个服务。
单独给秒杀服务创建一个秒杀库:秒杀订单,秒杀商品
单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他服务。

按钮控制

秒杀前按钮都是置灰的,秒杀时间到了才能点击。
按钮点击之后也给它置灰几秒,防止重复点击。
验证码登陆验证。

库存预热

秒杀的本质,就是对库存的抢夺。通过redis预扣库存的实现,避免了到底是支付减库存、下单减库存两种方案的选择,因为其各有利弊。而且还将验库存和减库存通过mq进行了解耦,极大的提升系统并发度和响应时间。使得系统从qps不到1000提升到上万的能力。

每个秒杀用户都去数据库查询库存效验库存,然后扣减库存,所有的操作都在数据库,会导致数据库顶不住

秒杀前通过定时任务将库存加载到redis中去,让效验库存的操作在redis中进行,然后发mq同步redis和数据库的数据。
在高并发场景下,多个线程同时效验该商品库存满足条件,然后同时扣减库存导致超卖问题发生。
Lua脚本功能时Redis2.6版本最大的亮点,通过对Lua脚本的支持,Redis解决了长久以来不能高效处理CAS命令的缺点,并且可以通过组合多个redis命令,轻松实现以前很难实现或者不能高效实现的模式。

Lua脚本实现了redis事务操作,我们将判断扣减库存的操作 和 预减库存的操作写到一个Lua脚本,返回扣减后的库存数量,如果库存数量<0则判断为发生超卖,将之前扣减的数目再加回去;如果库存数量>0,则发一个订单消息,然后订单模块生成订单,库存模块扣减库存,支付模块进行支付;支付成功后发送一个支付成功消息,然后优惠券模块扣减优惠券,积分模块增加积分,最后向用户发送短信通知。

JSONObject.parseObject(data);
库存=配额
key=手机号/微信ID+活动ID  value=订单数据
kafka的key=redis的key  value=orderId

通过Lua脚本实现抢红包功能,很优秀

Lua脚本示例

在这里插入图片描述

## 尝试获得红包,如果成功,则返回json字符串,如果不成功,则返回空
## eval函数(脚本名称,参数个数,keys1,keys2,keys3,keys4)
## keys1:预生成的红包队列 keys2: 已消费的红包队列 keys3: 去重map keys4:用户id
 static String tryGetHongBaoScript =   
        "if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then\n"  //如果用户已经抢过红包,则返回nill
        + "return nil\n"  
        + "else\n"  
        + "local hongBao = redis.call('rpop', KEYS[1]);\n"    //取出一个小红包
        + "if hongBao then\n"  
        + "local x = cjson.decode(hongBao);\n"   
        + "x['userId'] = KEYS[4];\n"                          //加入用户信息
        + "local re = cjson.encode(x);\n"  
        + "redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);\n"  //将用户放到去重的set中去,防止多次抢红包
        + "redis.call('lpush', KEYS[2], re);\n"               //将红包放入以消费队列中
        + "return re;\n"  
        + "end\n"  
        + "end\n"  
        + "return nil";  

static public void testTryGetHongBao() throws InterruptedException {  
    final CountDownLatch latch = new CountDownLatch(threadCount);  
    
    long startTime = System.currentTimeMillis();
    System.err.println("start:" + startTime);  
    
    for(int i = 0; i < threadCount; ++i) {  
        final int temp = i;  
        Thread thread = new Thread() {  
            public void run() {  
                Jedis jedis = new Jedis(host, port);  
                String sha = jedis.scriptLoad(tryGetHongBaoScript);  
                int j = honBaoCount/threadCount * temp;  
                while(true) {  
                    //抢红包方法
                    Object object = jedis.eval(tryGetHongBaoScript, 4, 
                            hongBaoList/*预生成的红包队列*/, 
                            hongBaoConsumedList, /*已经消费的红包队列*/
                            hongBaoConsumedMap, /*去重的map*/
                            "" + j  /*用户id*/
                            );  
                    j++;  
                    if (object != null) {  
                        //do something...
  //                          System.out.println("get hongBao:" + object);  
                    }else {  
                        //已经取完了  
                        if(jedis.llen(hongBaoList) == 0)  
                            break;  
                    }  
                }  
                latch.countDown();  
            }  
        };  
        thread.start();  
    }  

订单生成、库存扣减与支付逻辑

扣减库存的三种方案

刚开始我们使用的是付款减库存,防止恶意买家大量下单。
微信支付回调会返回微信生成的订单号以及我们自己生成的订单号

a) 设置的秒杀活动的库存,总是莫名其妙的减少了。分析发现是因为我们把减库存放在微信支付的成功回调里面的,而微信会回调这个url8次,导致多次减库存。最后我们通过接口幂等进行了解决
b) 用户下单显示的不是最新的数据库,支付时用户经常由于库存不足而支付失败,这导致用户体验十分不好。

  1. 下单减库存
    优点:实时减库存,避免付款时因库存不足减库存的问题
    缺点:恶意买家大量下单将库存用完,但是不付款,导致真正想买的人买不到
  2. 付款减库存
    优点:防止恶意买家大量下单
    缺点:下单显示的库存可能不是最新的库存数 ,支付时出现支付失败。多次回调问题。用户体验问题。
  3. 预扣库存(普通订单采用的方式)
    下单显示最新的库存,下单后保留这个库存一段时间(锁定库存),超过保留时间库存释放。超过再支付则支付失败
    优点:下单实时减库存,缓解恶意买家大量下单的问题,保留时间内未支付则释放库存
    缺点:保留时间内,恶意买家大量下单将库存用完。
    解决: 每个用户只能参加一个活动,购买一个商品。登陆验证码。
    1、创建订单,状态标记未支付,锁定库存
    2、支付完成后,标记订单状态完成,并减去库存
    3、支付超时,订单标记关闭,并释放库存
    4、订单取消,订单标记关闭,并释放库存
如何解决恶意买家大量下单问题
  1. 限制用户下单数量
  2. 标识恶意买家58
如何解决下单成功而支付失败的问题

备用库存:商品库用完之后,如果还有用户支付,则直接扣减备用库
优点:缓解部门用户支付失败问题
缺点:不能从根本解决问题,若并发量很大,还是会出现大量用户下单成功缺库存不足而支付失败的问题

支付成功多次回调:把减库存放在微信支付成功回调url里面

微信支付成功后,微信支付平台会发送8次回调地址,这样就得做接口幂等

秒杀链接加盐

链接要是提前暴露出去可能有人直接访问url就提前秒杀了
刚开始我们想到做个秒杀开始、截止时间,但是这种方案解决不了通过程序进行抢购。
将URL动态化,通过MD5之类的加密随机的字符串去做url,然后通过前端代码获取url后台效验才能通过

redis集群

redis集群,主从同步,读写分离。
开启持久化保证高可用

Nginx

Nginx是高性能web服务器,并发轻松上万并发,但普通Tomcat只能顶住几百的并发
买流量机
几万并发——>Nginx——>Tomcat集群

限流&降级&熔断

库存系统-可删减,作为商品的一个属性

保持商品库存数量的准确性是库存系统最根本的功能
在这里插入图片描述

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值