springboot开发银行秒杀后端系统——订单篇(上篇)

782 阅读4分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

之前我们已经完成了用户模块以及和shiro的整合,链接在下方:

# springboot开发银行秒杀后端系统——用户篇(下篇)

# springboot整合shiro——前后端分离模式(浅析登录流程)

项目代码同步更新在GitHub->github.com/yt-King/sec…

接下来就是秒杀系统的重头戏——秒杀部分,我设计的秒杀系统中一次完整的秒杀从点击活动详情开始,进入详情页之前后台会根据用户信息和商品配置的准入规则判断用户是否有参与活动的权限(会在后续更新具体操作),进入详情页后根据活动时间设置抢购按钮的点击属性(未在活动时间内设置按钮置灰不可点击),点击抢购按钮后生成临时订单用于支付,点击支付按钮后抢购成功将订单写入数据库。接下来要解决一下秒杀系统的几个问题,目前只是简单的处理,后续会继续优化。

1-超卖问题

对于秒杀活动中这种高效益的产品不可能无限量供应,在总额度有限的前提下,如果发生超卖的情况,不仅会损失金钱,也会引发用户投诉。所以首要的就是要防止商品超卖,目前我采用的做法就是加乐观锁,对于商品表我写了两个表,详细信息可以前往GitHub查阅源码,一个是完整的表用于活动展示传参,另一个是缩减不必要的字段专门在下单减库存的时候用的,如下图所示:

image.png

我为秒杀商品表增加了一个version字段用于更新,更新语句如下:

<update id="updateByOptimistic" parameterType="com.yt.seckill.entity.TSeckillGoods">
    update t_seckill_goods
    <set>
        goods_sale = goods_sale + 1,
        goods_version = goods_version + 1,
    </set>
    WHERE data_id = #{dataId,jdbcType=INTEGER}
    AND goods_version = #{goodsVersion}
</update>

语句更新成功的条件:只有版本号和之前找出来的一样,才能证明这个库存没有被别人更新过,才可以更新成功,否则的话报异常抛出即可,可以根据下图更好的理解乐观锁的原理。

image.png

在实现下单的过程中我们先进行库存的校验,然后再通过乐观锁更新库存,成功后在生成订单

image.png

但是通过乐观锁更新操作带来的问题是频繁的读写操作数据库压力很大,而且更新的成功率很低,会返回大量的version不匹配的报错,用户体验不是很好,可以通过限流的方式稍微优化一下。

2-令牌桶限流

可以看到令牌桶的作用就是在闲暇时往同理存一定数量的令牌,到达上限后溢出,当有请求打过来的时候就拿走一个令牌,当高压时期桶里的令牌都拿光了而且新的还没生成的时候有请求打过来就拒绝这个请求,这样子的话可以缓解后方的压力,但是对于用户来说体验可能还是不好,但至少保护了一下我们的数据库和后台。 image.png

接下来具体看一下实现流程,我这里使用Guava的RateLimiter实现令牌桶限流接口,首先在依赖里导入,我这里用的是gradle构建

implementation group: 'com.google.guava', name: 'guava', version: '30.1.1-jre'

然后再我们的下订单的controller层实例化一个RateLimiter对象,每秒生成的令牌数可以自行设定。

image.png

然后在下单的接口中使用,使用的时候有两种获取方式,我这里用的是非阻塞式获取,如果用阻塞式的话就会让请求一直等待,非阻塞式的会在等待时间过后直接返回失败信息。

@PostMapping("/createtemp")
@Operation(summary = "用户下单生成临时待支付订单")
public TOrderRecord createTempOrder(@RequestBody ParamTempDto params, HttpServletRequest request) throws Exception {
    //非阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,会尝试等待设置好的时间(这里写了1000ms),其会自动判断在1000ms后,
    //这个请求能不能拿到令牌,如果不能拿到,直接返回抢购失败。如果timeout设置为0,则等于阻塞时获取令牌。
    if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS))
        throw new RuntimeException("下单失败,限流");
    TOrderRecord tOrderRecord = tOrderRecordService.createTempOrder(params);
    return tOrderRecord;
}

以上只是部分问题的简单解决方法,后续还会继续优化,大家有什么好的建议欢迎提出。