非分布式秒杀系统 并发情况下解决超卖问题

乐观锁防止超卖 / 令牌桶限流/ redis缓存 /接口限流/接口加盐/单用户限制访问频率/消息队列异步处理订单

#数据库表drop table if exists `stock`;
create table `stock`(`id` int(11) unsigned not null auto_increment,`name` varchar(50) not null default '' comment '名称',`count` int(11) not null comment '库存',`sale` int(11) not null comment '已售',`version` int(11) not null comment '乐观锁,版本号',primary key(`id`)
)engine=innoDB default charset=utf8;drop table if exists `stock_order`;
create table `stock_order`(`id` int(11) unsigned not null auto_increment,`sid` int(11) not null comment '库存id',`name` varchar(30) not null default '' comment '商品名称',`create_time` timestamp not null default current_timestamp on update current_timestamp comment '创建时间',primary key(`id`)
)engine=innoDB default charset=utf8;
server.port=8080
server.servlet.context-path=/#数据库源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ms?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root#配置日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpllogging.level.root=info
logging.level.com.nmsl.dao=debug#mybatis配置
mybatis.mapper-locations=classpath:com/nmsl/mapper/*.xml
mybatis.type-aliases-package=com.nmsl.entity

1.悲观锁

@RestController
public class StockController {@Resourceprivate OrderService orderService;@GetMapping("/kill")public String kill(Integer id){    //秒杀方法System.out.println("秒杀商品的id = " + id);//根据秒杀商品的id去调用秒杀业务try {synchronized (this){//放在控制器调用的地方使用synchronizedint orderId = orderService.kill(id);return "秒杀成功,订单id为 = " + String.valueOf(orderId);}} catch (Exception e) {e.printStackTrace();return e.getMessage();}}/*** @author Paracosm*/
@Service
@Transactional
public class OrderServiceImpl implements OrderService {@Resourceprivate StockDao stockDao;@Resourceprivate OrderDao orderDao;//synchronized最好不要加在方法上,而是加在controller调用该方法的地方.//如果要加在方法上必须去掉事务注解@Transactional.//因为该方法还是被包含在事务内的, 事务并没有完成,还是有可能出现多提交的问题. 所以需要取消事务注解.//public synchronized int kill(Integer id) {   @Overridepublic  int kill(Integer id) {   //1.根据商品id校验库存Stock stock = checkStock(id);//2.扣除库存updateSale(stock);//3.创建订单return createOrder(stock);}//1.校验库存private Stock checkStock (Integer id){Stock stock = stockDao.checkStock(id);;if (stock.getSale().equals(stock.getCount())){throw new RuntimeException("库存不足");}return stock;}//2.扣除库存private void updateSale(Stock stock){stock.setSale(stock.getSale() + 1);stockDao.updateSale(stock);}//3.创建订单private Integer createOrder(Stock stock){Order order = new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());orderDao.createOrder(order);return order.getId();}
}

同步代码块如果在大量并发的情况下抢当前的线程锁.synchronized只会允许一个线程进行业务调用,后面的调用都会处于阻塞状态. 1.线程阻塞 2.系统效率吞吐量受影响 3.用户体验不好

2.乐观锁

主要是把并发压力都交给了数据库. 利用数据库中定义的version字段和事务来实现在并发情况下解决.超卖

更新方法改造

    private void updateSale(Stock stock){//在sql层面完成 sale+1 和 version+1 并根据商品id和版本号同时查询更新的商品int i = stockDao.updateSale(stock);//没有更新成功if (i == 0){throw new RuntimeException("抢购失败,请重试");}}
<-- update stock set sale=#{sale} where id=#{id}--><!--根据商品id扣除库存-->
<update id="updateSale" parameterType="Stock">update stockset sale=sale + 1,version=version + 1where id = #{id}and version = #{version};</update>

3.接口限流

​ 对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因为流量暴增导致的系统运行缓慢或者宕机.

如何解决接口限流?

  • 令牌桶 google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法.

    • 请求先进入漏桶中,漏桶以一定速度出水,当水流入速度过大会直接溢出,漏桶算法是强行限制数据的传输速率.
  • 漏桶(漏斗算法)
    • 最初来源于计算机网络,在网络传输数据时,为了防止网络阻塞需要限制流出网络的流量,使流量以比较均衡的速度向外发送. 令牌桶实现这个功能,可以控制发送到网络上的数据的数目,并允许突发数据的发送, 大小固定的令牌桶课自行以恒定的速率源源不断的产生令牌,如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多直到把桶填满.后面再产生的令牌就会从桶中溢出,最后桶中可以保存的最大令牌数永远不会超过桶的大小.这意味着面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情.

开发高并发系统时候的三把保护系统的利器: 缓存,降级,限流

  1. 缓存: 提升系统访问速度和增大系统处理容量

  2. 降级: 当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心人物的正常运行.

  3. 限流: 通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统, 一旦达到限制速率可以拒绝服务,排队或者等待,降级等处理.

    1.令牌桶

    RateLimiter

1.引入依赖<!--谷歌开源工具类 - RateLimter 令牌桶的实现--><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>29.0-jre</version></dependency>
@RestController
@RequestMapping("/stock")
@Slf4j
public class StockController {@Resourceprivate OrderService orderService;//创建令牌桶实例private RateLimiter rateLimiter = RateLimiter.create(10);//放行10个请求@GetMapping("/sale")public String sale(){//1.没有获取到token的请求一直阻塞,直到获取到tokenlog.info("等待的时间"+rateLimiter.acquire());//2.设置一个等待时间,如果在等待的时间内获取到了token令牌,则处理业务。如果超时则抛弃if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){System.out.println("当前请求被限流,直接抛弃");return "抢购超时";}System.out.println("处理业务......");return "抢购成功";}

2.令牌桶实现乐观锁+限流

//创建令牌桶实例 每次放行10个token 限流
private RateLimiter rateLimiter = RateLimiter.create(10);//令牌桶实现乐观锁+限流@GetMapping("/killtoken")public String killtoken(Integer id){    //秒杀方法System.out.println("秒杀商品的id = " + id);//根据秒杀商品的id去调用秒杀业务if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {log.info("抛弃请求:抢购失败啦!");return "抢购超时,请重试!";}else {try {//放在控制器调用的地方使用synchronizedint orderId = orderService.kill(id);return "秒杀成功,订单id为 = " + String.valueOf(orderId);} catch (Exception e) {e.printStackTrace();return e.getMessage();}}}

4.隐藏秒杀接口

  1. 一定时间内执行秒杀处理吗,不能在任意时间接收秒杀请求,如何加入实践验证? 限时抢购 redis记录时间,setex
  2. 如果遇到抓包获取接口地址,再通过脚本抢购的怎么办? 抢购接口隐藏
  3. 单用户限制频率 (单位时间内限制访问次数)

1.redis记录秒杀时间

#将秒杀商品放入redis并设置超时   setex kill+id time id
setex kill1 180 1`定时任务需要加@EnableScheduling注解 具体可以看官方文档`
cron表达式是指定时任务触发时间的字符串表达式,分为 6 或 7 个域,每一个域代表一个含义语法: Seconds Minutes Hours Day Month Week
<--加入redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
#redis配置
spring.redis.database=0
spring.redis.port=6379
spring.redis.host=localhost
 @Resourceprivate StringRedisTemplate stringRedisTemplate;public  int kill(Integer id) {//加入Redis限时处理,是否在redis中有效? 对秒杀过期的请求进行拒绝处理//校验redis中秒杀商品是否超时Boolean key = stringRedisTemplate.hasKey("kill" + id);if (!key){        //如果存在,说明还在秒杀的范围内.不存在说明超时了throw new RuntimeException("当前商品抢购活动已结束!");}//1.根据商品id校验库存Stock stock = checkStock(id);//2.扣除库存updateSale(stock);//3.创建订单return createOrder(stock);}

2.隐藏接口 (接口加盐)

  • 每次点击秒杀按钮,先从服务器获取一个秒杀的验证值(接口内判断是否到秒杀时间)
  • Redis以缓存用户id和商品id为key,秒杀地址为value缓存验证值
  • 用户请求秒杀商品的时候要带上秒杀验证值进行校验.
#表结构set names utf8mb4;
set foreign_key_checks = 0;drop table if exists `user`;
create table `user`(`id` int(11) not null auto_increment comment '主键',`name` varchar(80) default null comment '用户名',`password` varchar(40) default null comment '用户密码',primary key(`id`)
)engine=InnoDB auto_increment=2 default charset=utf8;set foreign_key_checks = 1 ;
   //Controller层//获取md5@RequestMapping("/md5")public String getMd5(Integer id,Integer userId){String md5;try{md5 = orderService.getMd5(id,userId);}catch (Exception e){e.printStackTrace();return "获取md5失败" + e.getMessage();}return "获取md5的数值为: " + md5;}//实现类@Resourceprivate UserDao userDao;@Overridepublic String getMd5(Integer id, Integer userid) {//验证userid用户合法性User user = userDao.findById(userid);if (user==null) {throw new RuntimeException("用户信息不存在");}log.info("用户信息:[{}]",user.toString());//验证id 商品合法性Stock stock = stockDao.checkStock(id);if (stock==null) {throw new RuntimeException("商品信息不合法");}log.info("商品信息:[{}]",stock.toString());//生成hashkeyString hashKey = "KEY_" + userid + "_" + id;//生成md5签名放入redis    这里!QS#是一个盐 随机生成String key = DigestUtils.md5DigestAsHex((userid + id + "!Q*jS#").getBytes());stringRedisTemplate.opsForValue().set(hashKey,key,3600, TimeUnit.SECONDS);log.info("redis写入:[{}][{}]", hashKey, key);return key;}===========================================================================================
<select id="findById" parameterType="Integer" resultType="User">select id,name,password from user where id=#{id}</select>

​ Controller层代码

//令牌桶实现乐观锁+限流+redismd5缓存@GetMapping("/killtokenmd5")public String killtokenMd5(Integer id,Integer userId,String md5){    //秒杀方法System.out.println("秒杀商品的id = " + id);//根据秒杀商品的id去调用秒杀业务if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {log.info("抛弃请求:抢购失败啦!");return "抢购超时,请重试!";}else {try {//放在控制器调用的地方使用synchronizedint orderId = orderService.kill(id,userId,md5);return "秒杀成功,订单id为 = " + String.valueOf(orderId);} catch (Exception e) {e.printStackTrace();return e.getMessage();}}}

​ Service层代码:

//加了md5签名的@Overridepublic int kill(Integer id, Integer userid, String md5) {//验证超时//Boolean key = stringRedisTemplate.hasKey("kill" + id);//如果存在,说明还在秒杀的范围内.不存在说明超时了//if (!key) {//    throw new RuntimeException("当前商品抢购活动已结束!");//}//验证签名 (开发中应该是在验证超时之后执行的)String hashKey = "KEY_" + userid + "_" + id;if (!md5.equals(stringRedisTemplate.opsForValue().get(hashKey))) {throw new RuntimeException("当前请求数据不合法,请稍后再试.");}//1.根据商品id校验库存Stock stock = checkStock(id);//2.扣除库存updateSale(stock);//3.创建订单return createOrder(stock);}

其实也可以可以key在存入reids的同时记录一个访问次数,超过次数就拒绝访问

单用户限制访问频率

service接口

public interface UserService {//向redis写入用户访问次数int saveUserCount(Integer UserId);//判断单位时间内调用次数boolean getUserCount(Integer UserId);
}

实现类

@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic int saveUserCount(Integer userId) {//根据不同用户id生成调用次数的keyString limitKey = "LIMIT" + "_" + userId;//获取redis中指定key的调用次数String limitNum = stringRedisTemplate.opsForValue().get(limitKey);int limit = -1;if (limitNum == null) {//第一次调用时放入redis中设置为0stringRedisTemplate.opsForValue().set(limitKey,"0",60, TimeUnit.SECONDS);}else {//已经不是第一次调用,每次调用就+1limit = Integer.parseInt(limitNum) + 1;stringRedisTemplate.opsForValue().set(limitKey,String.valueOf(limit),60,TimeUnit.SECONDS);}return limit;}@Overridepublic boolean getUserCount(Integer userId) {//根据用户id对应的key获取调用次数String limitKey = "LIMIT" + "_" + userId;//跟库用户调用次数的key获取redis中调用次数String limitNum = stringRedisTemplate.opsForValue().get(limitKey);if (limitNum == null) {//为空直接抛弃,说明key出现异常log.error("该用户没有访问申请md5验证值记录,疑似一场请求");return true;}//false代表没有超过,true代表超过return Integer.parseInt(limitKey) > 10;}
}

controller层

     //令牌桶实现乐观锁+限流+redismd5缓存验证+隐藏接口+单用户限流@GetMapping("/killtokenmd5limit")public String killtokenmd5limit(Integer id, Integer userId, String md5) {//加入令牌桶限流措施if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)) {log.info("抛弃请求:抢购失败啦!");return "抢购超时,请重试!";}else {try {//单用户调用接口频率限制int count = userService.saveUserCount(userId);log.info("用户截至该次的访问数量:[{}]", count);//进行调用次数的判断.如果为true购买失败,false可以继续执行boolean isBanned = userService.getUserCount(userId);if (isBanned) {log.info("购买失败,超过频率限制");return "购买失败,超过频率限制";}int orderId = orderService.kill(id,userId,md5);return "秒杀成功,订单id为 = " + String.valueOf(orderId);} catch (Exception e) {e.printStackTrace();return e.getMessage();}}}

ps:学习不良人老师的秒杀系统视频

秒杀系统并发情况下解决超卖问题相关推荐

  1. 高并发情况下解决单用户超领优惠券问题

    问题抛出 在近期的项目里面有一个功能是领取优惠券的功能, 问题描述: 每一个优惠券一共发行多少张,每个用户可以领取多少张: 如:A优惠券一共发行120张,每一个用户可以领取140张,当一个用户领取优惠 ...

  2. 【秒杀业务思路-缓存预热-防止超卖】

    秒杀业务 1,开发查询秒杀商品列表的功能 2,开发根据spuid查询秒杀sku列表信息 3,根据当前时间查询正在秒杀的商品 4,根据spuid查询秒杀商品详情 5,查询所有秒杀商品的spuid 6,缓 ...

  3. 阿里面试官:高并发大流量秒杀系统如何正确的解决库存超卖问题?(建议收藏)

    大家好,我是冰河~~ 在[精通高并发系列]的<实践出真知:全网最强秒杀系统架构解密!!>一文中,冰河详细的阐述了高并发秒杀系统的架构设计,也简单提到了如何扣减商品的库存. 也许不少小伙伴会 ...

  4. MySql(15)——Mysql在高并发情况下,防止库存超卖而小于0的解决方案

    本人上次做申领campaign的PHP后台时,因为项目上线后某些时段同时申领的人过多,导致一些专柜的存货为负数(<0),还好并发量不是特别大,只存在于小部分专柜而且一般都是-1的状况,没有造成特 ...

  5. 基于秒杀系统解决超卖、限流、Redis限时抢购等问题

    完整项目请见:https://gitee.com/JiaBin1 一.什么是秒杀 秒杀最直观的定义:在高并发场景下而下单某一个商品,这个过程就叫秒杀 [秒杀场景] 火车票抢票 双十一限购商品 热度高的 ...

  6. Mysql在高并发情况下,防止库存超卖而小于0的解决方案

    背景: 本人上次做申领campaign的PHP后台时,因为项目上线后某些时段同时申领的人过多,导致一些专柜的存货为负数(<0),还好并发量不是特别大,只存在于小部分专柜而且一般都是-1的状况,没 ...

  7. 在高并发情况下如何解决用户超领优惠券问题

    在高并发情况下如何解决单用户超领优惠券问题 一. 场景描述 在近期的项目里面有一个功能是领取优惠券的功能,每一个优惠券一共发行多少张,每个用户可以领取多少张: 如:A优惠券一共发行120张,每一个用户 ...

  8. 秒杀系统优化以及解决超卖问题

    问题描述 在众多抢购活动中,在有限的商品数量的限制下如何保证抢购到商品的用户数不能大于商品数量,也就是不能出现超卖的问题:还有就是抢购时会出现大量用户的访问,如何提高用户体验效果也是一个问题,也就是要 ...

  9. 使用Redis分布式锁处理并发,解决超卖问题

    使用Redis分布式锁处理并发,解决超卖问题 参考文章: (1)使用Redis分布式锁处理并发,解决超卖问题 (2)https://www.cnblogs.com/VitoYi/p/8726070.h ...

最新文章

  1. 现在的编译器还需要手动展开循环吗_性能 - 如果有的话,循环展开仍然有用吗?...
  2. R语言setdiff函数集合作差运算实战
  3. 家长不知道孩子在学校学的咋样,怎么能得行?
  4. 不让自己的应用程序在桌面的图标列表里启动显示的方法
  5. 湖南单招计算机专业大学排名,2021湖南单招学校排名及分数线:湖南单招分数线高吗?...
  6. 给ListView视图添加行号
  7. Lucene分词器,使用中文分词器,扩展词库,停用词
  8. 少儿编程家长疑问解答
  9. Solved: RDP Disconnected – Error Code 2825 mremote
  10. 树莓派+aria2+yaaw搭建下载机
  11. HTML中绑定点击事件的方式
  12. menu什么意思中文意思_menu是什么意思
  13. 编程猫python讲师面试_【编程猫教师面试】笔试:试题+打字测速-看准网
  14. epub是什么文件?epub文件怎么打开?
  15. Ubuntu / Python / Mega自动同步监控照片
  16. 所见即所得html5编辑器,一个漂亮的所见即所得(WYSIWYG)富文本编辑器:Froala
  17. 助力智能网联发展,中认车联网与怿星科技合作实验室正式揭牌
  18. DBSCAN: 基于密度对空间含噪声数据中不规则形状进行聚类
  19. There are no devices registered in your account on the developer website
  20. GridView 分页导航不显示

热门文章

  1. 软件设计师——软件工程
  2. 看了几个NHK的记录片
  3. 安装oracle高级安装,oracle R11g高级安装详细教程
  4. 解决Win11微软拼音输入法导致Shift+F6/Shift+F10失效问题
  5. 说说React生命周期中有哪些坑?如何避免?
  6. js和jsp文件后缀还在傻傻分不清?一文教你搞懂来龙去脉
  7. 关键信息基础设施面临的安全威胁
  8. wav格式怎么转换?介绍三个转换wav格式的方法
  9. python全栈测试开发_python全栈的基础知识
  10. JAVA毕业设计智慧医疗医患交流系统设计计算机源码+lw文档+系统+调试部署+数据库