微服务高并发秒杀实战_基于令牌桶实现库存

什么是秒杀

秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。

秒杀系统场景特点

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
  • 秒杀业务流程比较简单,一般就是下订单减库存。

解决高并发(秒杀),我们应该从前端,后台统筹考虑,下面详细讲解具体实现方式:

一 . 前端优化

1. 采用动静分离架构(前端优化核心)

动态数据(后台代码)一般放在ECS等云服务器,静态数据(js/css/img实现压缩减少带宽的传输)放入七牛云或者阿里云OSS等第三方资源服务器,并使用CDN加速,从而减少客户端与服务端带宽传输。

如下图所示一般详情页的图片是不变的,可以直接在页面写死即可,如果想做成动态的,只要保证后台数据库商品图片存的路径为oss即可,这样可以减少图片传输所占用的带宽。

**2. 防止重复提交:**提交后按钮disabled,防止用户重复提交

**3. 商品详情页面:**使用Nginx+Lua+OpenResty实现商品详情页面的优化(需要掌握lua语言,不在本文讨论范围内)

二. 后端优化

1. 基于乐观锁/redis锁,防止库存超卖

这里拿乐观锁举例,redis锁原理都是一样的

① 不加版本号的行级悲观锁(多个用户操作一个商品,线程本身就是安全的,因为数据库默认开启行级锁)

@Update("update goods_table set inventory=inventory-1 where inventory > 0 and goods_id=#{goodsId}")
int inventoryDeduction(@Param("goodsId") Long goodsId);

② 带版本号的乐观锁(数据库需单独新增一个version字段,每次调接口,version加1)

@Select("SELECT order_id,goods_name,inventory,create_time,version from gooods_table where goods_id=#{goodsId}")
GoodsDto getByGoodsId(@Param("goodsId")Long goodsId);
@Update("update goods_table set inventory=inventory-1,version=version+1 where inventory > 0 and goods_id=#{goodsId} and version=#{version}")
int inventoryDeduction(@Param("goodsId")Long goodsId,@Param("version")Long version);
@Transactional
public BaseResponse<JSONObject> spike(String phone, Long GoodsId) {
    GoodsDto goodsDto = seckillMapper.getByGoodsId(GoodsId);
    if (goodsDto == null) {
        return setResultError("商品信息不存在!");
    }
    Long version = goodsDto.getVersion();// 先获取版本号
    int row = seckillMapper.inventoryDeduction(GoodsId, version);// 减库存
    // 后续可以生成订单到订单表。。。
}

使用Jmeter工具压测一下,库存初始化100,模拟200个请求,结论是无论如何都不会出现库存为0的现象(超卖)

但方式①,200个请求,会有100个抢购成功,剩余库存为0,方式②,200个请求只有66个请求成功,剩余库存为34。

实际开发中也可以使用redis锁进行控制,详细参考博主之前的博客!

2. 基于Redis对用户实现频率限制(限流一般都会在网关做)

为了防止某个客户端一直刷下单接口,可以基于redis的setNx命令,实现抢购时用户的频率限制:

@Transactional
public BaseResponse<JSONObject> spike(String phone, Long GoodsId) {
    GoodsDto goodsDto = seckillMapper.getByGoodsId(GoodsId);
    if (goodsDto == null) {
        return setResultError("商品信息不存在!");
    }
    // 用户频率限制 setnx 如果key存在话
    Boolean reusltNx = redisUtil.setNx(phone, seckillId + "", 10l);
    if (!reusltNx) {
        return setResultError("访问次数过多,10秒后再实现重试!");// 直接return,无需执行下面的version++操作
    }
    Long version = goodsDto.getVersion();// 先获取版本号
    int row = seckillMapper.inventoryDeduction(GoodsId, version);// 减库存
    // 后续可以生成订单到订单表。。。
}

@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // 如果key存在的话返回fasle 不存在的话返回true(原生redis的setNx命令会返回0或1)
    public Boolean setNx(String key, String value, Long timeout) {
	Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
	if (timeout != null) {
	    stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
	}
	return setIfAbsent;
    }

    public void setList(String key, List<String> listToken) {
        stringRedisTemplate.opsForList().leftPushAll(key, listToken);
    }
}

    

3. 基于库存令牌桶 + MQ 实现异步修改库存并提交订单

在高并发情况下,如果有1万个用户同时秒杀某一商品,对数据库频繁的IO操作,可能会产生数据库崩溃问题(分表分库,读写分离治标不治本),解决方法:基于MQ+库存令牌桶实现。

同时有1万个请求实现秒杀,商品库存只有100个, 实现只需要修改库存100次就可以了:

方案实现流程:商品库存是多少,就提前在redis生成多少对应的库存令牌(这里即为100),在1万个请求中,只要谁能够获取到令牌谁就能够秒杀成功, 获取到秒杀令牌后,在使用mq异步实现修改减去库存。

代码实现:

① 编写生成100个令牌桶接口

    

② 编写获取token代码,并用mq异步发送

RabbitMQ消费者

@Component
Slf4j
public class StockConsumer {
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private OrderMapper orderMapper;
    @RabbitListener(queues = "modify_inventory_queue")
    @Transactional
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
	String messageId = message.getMessageProperties().getMessageId();
	String msg = new String(message.getBody(), "UTF-8");
	JSONObject jsonObject = JSONObject.parseObject(msg);
	// 1.获取秒杀id
	Long goodsId = jsonObject.getLong("seckillId");
	SeckillEntity seckillEntity = seckillMapper.findBySeckillId(goodsId);
	if (seckillEntity == null) {
	    log.warn("goodsId:{},商品信息不存在!", goodsId);
	    return;
	}
	Long version = seckillEntity.getVersion();
        // 2.减库存
	int inventoryDeduction = seckillMapper.inventoryDeduction(goodsId, version);
	if (!toDaoResult(inventoryDeduction)) {
	    log.info(">>>seckillId:{}修改库存失败", goodsId);
	    return;
	}
	// 3.添加订单
	OrderEntity orderEntity = new OrderEntity();
	String phone = jsonObject.getString("phone");
	orderEntity.setUserPhone(phone);
	orderEntity.setSeckillId(goodsId);
	orderEntity.setState(1l);
	int insertOrder = orderMapper.insertOrder(orderEntity);
	if (!toDaoResult(insertOrder)) {
	    return;
	}
	log.info(">>>成功消费seckillId:{},秒杀成功!", goodsId);
    }
    // 调用数据库层判断
    public Boolean toDaoResult(int result) {
        return result > 0 ? true : false;
    }
}

③  提供一个根据用户信息查询秒杀结果接口(实际开发中,也可以根据userId)

@GetMapping("/checkSpike")
public BaseResponse<JSONObject> getOrder(String phone, Long goodsId) {
    if (StringUtils.isEmpty(phone)) {
	return setResultError("手机号码不能为空!");
    }
    if (goodsId== null) {
    	return setResultError("商品库存id不能为空!");
    }
    OrderEntity orderEntity = orderMapper.findByOrder(phone, goodsId);
    if (orderEntity == null) {
**先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7**

**深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。**
![img](https://img-blog.csdnimg.cn/img_convert/7c5acb4a28fd81c42815dbe3cc9f16b8.png)
![img](https://img-blog.csdnimg.cn/img_convert/7278663303283ee5275b4f2bd8f6faa3.png)
![img](https://img-blog.csdnimg.cn/img_convert/1a9ce16bcaa5365d802d88060ae95a71.png)
![img](https://img-blog.csdnimg.cn/img_convert/83a5b38635490b285f561093d93e0e16.png)
![img](https://img-blog.csdnimg.cn/img_convert/d4ef195d8e82e6a11cafd197dcd3c5b3.png)
![img](https://img-blog.csdnimg.cn/img_convert/b4f8639fa0c4f0321a131e6e54d66f6c.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/topics/618540462)**
-1714367916178)]
[外链图片转存中...(img-DgBtraXM-1714367916179)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/topics/618540462)**
  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值