如何优雅地设计一个秒杀接口

秒杀接口主要考虑的几个关键问题是数据库超卖、接口限流,让系统能抗住大并发量,抛出非法请求等。

  • 乐观锁方式超卖
    在大并发量下,首先要考虑的问题就是线程安全问题。秒杀的三个步骤大致可分为 1.根据商品id查询商品的库存 2.如果库存充足扣减库存并更新 3.生成订单。
    为什么会出现超卖问题?

    假设总库存是100,第一个线程进来,查询没问题,并且让商品对象的库存减一,此时的库存数量更新为99,但是,没有更新数据库,这个线程的时间片就用完了

    第二个线程进来,查询没问题,并且让商品对象的库存减一
    此时的库存数量更新为99,但是第二个线程还是没有更新数据库,这个线程的时间片就用完了

    第三个线程进来,查询没问题,并且让商品对象的库存减一
    此时的库存数量更新为99,假设这个线程更新了数据库,那此时的数据库数量就是99,
    此时第一个线程执行,又更新为了99,第二个线程执行,又更新为了99,所以这里产生了三个订单,但数据库商品的库存才扣减了1个。

    解决办法
    1.使用 synchronized
    2. 使用乐观锁
    synchronized是一种悲观锁,会将整个方法同步,效率较低,而且synchronized和@Transactional注解一起使用时可能还会出现超卖问题。原因是@Transactional 本身也会有一个线程同步,而事务的这个线程同步的范围比sync的范围要大,也就是,sync释放了锁之后,这个事务线程控制的同步可能还没有提交下一个线程就进来了,也就是下一个线程可能会查询到上一个事务还没有提交的库存数量。 所以不要在业务层使用synchronized,因为可能会和事务注解产生问题,如果一定要用的话,可以使用在controller上。

    乐观锁的原理是在数据库每条记录上加一个version字段,然后更新的时候不但要更新库存,且每更新一次就把version加一,更新的条件是商品的id和之前的版本号。SQL示例如下:

    <update id="updateStockByVersion">
        UPDATE stock set  sale = sale + 1 , version = version + 1
          where id = #{id} and version = #{version}
    </update>

由于数据库在更新时也会加锁,所以每一时刻只有一条SQL执行更新。我们按照刚才synchronized的流程来分析以下,首先第一个线程进来,查询库存假设第一个线程的库存是100,版本号是0,在更新SQL时没有了时间片,此时第二个线程进来,同样库存是100,版本号是0,在更新SQL时没有了时间片,第三个线程进来库存是100,版本号是0,第三个线程进行更新,将库存变为99,版本号+1变为1,但是更新的条件是id = 商品id and version = 当前线程的版本号,第三个线程的版本号是0,而数据库初始的版本号也是0,第三个线程更新成功,此时再第二个线程更新,由于第二个线程更新的时候的version为0,但数据库的version已经成为1,则更新失败。版本号的方式是一种乐观锁,因为大量的请求可能在判读库存这一步就由于库存不足返回了。

  • 令牌桶实现限流

令牌桶算法的概述是每固定一段时间内产生一定数量的令牌,只有拿到令牌的线程才可以执行,如果拿不到可以等待,也可以规定一个时间,在某个时间窗口内拿不到就拒绝请求。

pom.xml引入令牌桶的实现。

		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>28.0-jre</version>
		</dependency>

这个依赖提供了一个实现类:RateLimiter。
1.RateLimiter.create(), acquire方法
该方法可以传递一个参数,表示一秒内可以生成多少令牌,只有获取到令牌的线程才可以执行。
2.tryAcquire方法
该方法可以传递一个时间参数,表示多少单位时间内获取不到令牌就抛弃。

  • Redis缓存
    在下单之前,可以先去redis中查询是否有这个要秒杀的商品,如果有,再继续下单,如果没有则拒绝下单。这里可能需要使用定时任务,在商品即将要开始秒杀的时候上线到Redis。

  • 接口隐藏
    为了避免某些人可以恶意请求接口,在请求下单接口时需要携带一个MD5的令牌,而这个令牌可以在下单前先发送请求获取一下,我们将令牌同时存储到Redis中一份,并且验证redis中与请求携带的令牌是否一致。这样可以避免恶意请求秒杀接口,因为如果要获取秒杀令牌是首先需要登录的。

  • 限制单一用户的访问次数

针对于某个固定的用户在某一时间内访问频繁,我们可以使用redis来限制其访问次数。

@Override
    public boolean limitUser(Integer userId) {
        //判断用户的访问次数
        String count = redisTemplate.opsForValue().get(PREFIX + userId);
        if (StringUtils.isEmpty(count)){
            //单位时间内第一次访问
            redisTemplate.opsForValue().set(PREFIX + userId,"1",TIME_SECOND, TimeUnit.SECONDS);
            return true;
        }else {
            //单位时间内不是第一次访问
            int currentCount = Integer.parseInt(count);
            if (currentCount >= COUNT){
                return false;
            }else {
                //单位时间内没有达到访问次数的限制
                currentCount++;
                //更新的时候重新更新过期时间
                Long expire = redisTemplate.getExpire(PREFIX + userId);
                redisTemplate.opsForValue().set(PREFIX + userId,currentCount + "",expire,TimeUnit.SECONDS);
                return true;
            }
        }
    }

主要流程是:首先我们需要规定每个用户在单位时间内的最大访问次数,如每个用户在20秒内最多可以访问5次,那么当用户请求接口时,根据用户Id,我们判断是否为第一次访问,如果为第一次访问,我们给redis中设置一个key,key要与用户id相关,而value就是访问此时,注意,要同时设置一个过期时间,而且在更新的时候也要获取过去时间设置,否则这个key就成了永不过期了。

以上五点设计在高并发接口下可以参考。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值