一,秒杀系统

1,秒杀场景

电商抢购限量商品

抢购演唱会的门票

火车票抢座12306

2.为什么要做个系统

如果项目流量非常小,完全不用担心并发请求的购买,那么做这样一个系统的意义并不大。但是如果你的系统要是像12306一样,接受高并发访问和下单的考验,那么你就需要一套完整的流程保护措施,来保证你系统在用户流量高峰期不会被搞挂了。

严格防止超卖:库存一百件,卖出去120件。

防止黑产:一个人全买了,其他人啥也没有。

保证用户体验:高并发下,网页打不开,支付不成功,购物车进不去,地址改不了,这个问题非常之大,涉及到各种技术。

3.保护措施有哪些

乐观锁防止超卖

令牌桶限流

redis缓存

消息队列异步处理订单

二,无锁状态下的秒杀系统

1.业务流程分析

1.前端接受一个秒杀请求传递到后端控制器

2.控制器接受请求参数,调用业务创建订单

3.业务层需要检验库存,扣除库存,(判断用户是否重复购买),创建订单

2.搭建项目

sql脚本

CREATE TABLE `ms_order` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单表',`product_id` int(11) DEFAULT NULL COMMENT '商品id',`create_time` datetime DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;CREATE TABLE `ms_stock` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存表',`product_id` int(11) DEFAULT NULL COMMENT '商品id',`product_name` varchar(255) DEFAULT NULL COMMENT '商品名称',`sum` int(11) DEFAULT NULL COMMENT '商品数量',`sale` int(11) DEFAULT NULL COMMENT '售出数量',`version` int(11) DEFAULT '0' COMMENT '版本号',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

pom依赖

    <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.yhd</groupId><artifactId>ms</artifactId><version>0.0.1-SNAPSHOT</version><name>ms</name><description>ms project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.48</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.2</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.0.5</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional><version>1.18.8</version></dependency></dependencies>

实体类

@NoArgsConstructor
@AllArgsConstructor
@ToString
@TableName("ms_order")
//开启链式调用
@Accessors(fluent = true)
@Data
public class Order implements Serializable {@TableId(type = IdType.AUTO)private Integer id;private Integer productId;private Date createTime;
}@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@TableName("ms_stock")
//开启链式调用
@Accessors(fluent = true)
public class Stock  implements Serializable {private Integer id;private Integer productId;private String productName;private Integer sum;private Integer sale;private Integer version;
}

mapper

public interface OrderMapper extends BaseMapper<Order> {}public interface StockMapper extends BaseMapper<Stock> {}

service

@Service
public class OrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StockMapper stockMapper;/*** 1.验证库存* 2.修改库存* 3.创建订单* @param productId* @return*/@Transactionalpublic Order Qg(Integer productId) {Stock product = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_id", productId));if (product.sum().equals(product.sale())){throw  new RuntimeException("抢购失败,商品已经卖光!");}else{stockMapper.updateById(product.sale(product.sale()+1));Order order = new Order();orderMapper.insert(order.createTime(new Date()).productId(productId));return order;}}
}

controller

@RestController
@RequestMapping("order")
public class OrderController {@Resourceprivate OrderService orderService;/*** 用户点击抢购,开始下单*/@GetMapping("qg/{productId}")public Order Qg(@PathVariable("productId") Integer productId){return orderService.Qg(productId);}
}

配置文件

server.port=8888
server.servlet.context-path=/msspring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql:///ms?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8mybatis-plus.type-aliases-package=com.yhd.ms.domain
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

3.jmeter压测工具

没有并发的情况下能够正常生成订单,但是当产生并发请求的时候,就会发生超卖问题。

三,单机下使用悲观锁解决超卖问题

首先因为synchronized是本地锁,如果是集群模式下,这样加锁是无法解决超卖的。

1.synchronized和事务的小问题

    @Transactionalpublic synchronized Order Qg(Integer productId) {Stock product = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_id", productId));if (product.sum().equals(product.sale())){throw  new RuntimeException("抢购失败,商品已经卖光!");}else{stockMapper.updateById(product.sale(product.sale()+1));Order order = new Order();orderMapper.insert(order.createTime(new Date()).productId(productId));return order;}}

单机模式下,我们在业务层代码加上synchronized关键字,加上以后发现并没有解决超卖问题,原因是synchronized这把锁是在事务里面的一部分,释放锁以后,实际上事务并未执行完,当事务提交,还是会修改数据库,相当于锁白加了。

2.解决方案

第一种方法就是吧事务去掉,但是业务层代码不加事务的问题就不用多描述了。所以采用第二种

第二种:

    /*** 用户点击抢购,开始下单*/@GetMapping("qg/{productId}")public Order Qg(@PathVariable("productId") Integer productId){synchronized (this) {return orderService.Qg(productId);}}
    /*** 1.验证库存* 2.修改库存* 3.创建订单** @param productId* @return*/@Transactionalpublic Order Qg(Integer productId) {Stock stock = checkStock(productId);updateStock(stock);Order order = createOrder(stock);return order;}/*** 验证库存** @param productId* @return*/private Stock checkStock(Integer productId) {Stock product = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_id", productId));if (product.sum().equals(product.sale())) {throw new RuntimeException("抢购失败,商品已经卖光!");}return product;}/*** 更新库存** @param stock* @return*/private Integer updateStock(Stock stock) {return stockMapper.updateById(stock.sale(stock.sale() + 1));}/*** 创建订单** @param stock* @return*/private Order createOrder(Stock stock) {Order order = new Order();orderMapper.insert(order.createTime(new Date()).productId(stock.productId()));return order;}

这次成功的解决了超卖问题,但是同时悲观锁也带来了效率低下的问题。

四,单机下使用乐观锁解决超卖问题

使用乐观搜解决商品超卖问题,实际上是把主要防止超卖问题交给数据库解决,利用数据库中定义的version字段以及数据库中的事务实现在并发情况下商品超卖问题。

select * from ms_stock where id =1 and version =0;

update ms_stock set sale=sale+1,version=version+1 where id=#{id} and version =#{version}

经过压力测试,发现不但解决了超卖问题,效率上也得到了很大的提高,但是当请求数量在一秒钟上升到20000个的时候,可以看到,系统崩溃了。

五,令牌桶接口限流防止系统崩溃

1.接口限流

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

在面临高并发的抢购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大压力。大量的请求抢购成功时需要调用下单接口,过多的请求打到数据库会对系统的稳定性造成影响。

2.如何解决接口限流

常用的限流算法有令牌桶算法和漏桶算法,而谷歌的开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。在开发高并发系统时有三把利器保护系统:缓存,降级和限流。

缓存:缓存的目的是提升系统访问速度和增大系统的处理容量。

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

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

3.漏桶算法和令牌桶算法

漏桶算法:请求先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

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

4.令牌桶使用案例

    /*** 创建令牌桶*/private RateLimiter rateLimiter=RateLimiter.create(10);/*** 测试令牌桶算法** @return*/@GetMapping("test")public String testLpt(){if (rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){log.info("争抢令牌消耗的时间为:" +rateLimiter.acquire());//模拟处理业务逻辑耗时try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return "成功抢到令牌,消耗时间为:+" +rateLimiter.acquire();}log.info("未抢到令牌,无法执行业务逻辑!");return "未抢到令牌,无法执行业务逻辑!";}

5.使用令牌桶优化秒杀系统

private RateLimiter rateLimiter=RateLimiter.create(10);/*** 用户点击抢购,开始下单*/@GetMapping("qg/{productId}")public String Qg(@PathVariable("productId") Integer productId) {if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){return "抢购失败,请重试!";}orderService.Qg(productId);return "抢购成功,耗时为:"+rateLimiter.acquire();}

六,隐藏秒杀接口

解决了超卖和限流问题,还要关注一些细节,此时秒杀系统还存在一些问题:

1.我们应该在一定的时间内执行秒杀处理,不能在任意时间都接受秒杀请求。如何加入时间验证?

2.对于稍微懂电脑的人,又会通过抓包的方式获取我们的接口地址,我们通过脚本进行抢购怎们么办?

3.秒杀开始之后如何限制单个用户的请求频率,即单位时间内限制访问次数?

1.使用redis实现限时抢购

    @Resourceprivate StringRedisTemplate redisTemplate;@Transactionalpublic Order Qg(Integer productId) {checkTime(productId);Stock stock = checkStock(productId);updateStock(stock);Order order = createOrder(stock);return order;}/*** 使用redis实现限时抢购*/public void checkTime(Integer productId){Boolean flag = redisTemplate.hasKey("SECOND_KILL" + productId);if (!flag){throw new RuntimeException("秒杀活动已经结束,欢迎下次再来!");}}

2.秒杀接口的隐藏处理

我们需要将秒杀接口进行隐藏的具体方法:

每次点击秒杀按钮,实际上是两次请求,第一次先从服务器获取一个秒杀验证值(接口内判断是否到秒杀时间)

redis以缓存用户ID和商品ID为key,秒杀地址为value缓存验证值

用户请求秒杀商品的时候,要带上秒杀验证值进行校验

加入用户表

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@TableName("ms_user")
//开启链式调用
@Accessors(fluent = true)
public class User implements Serializable {private Integer id;private String name;private String pwd;
}

生成MD5接口

    /*** 生成MD5接口*/@GetMapping("md5/{pid}/{uid}")public String createMD5(@PathVariable("pid")Integer pid,@PathVariable("uid")Integer uid){return orderService.createMD5(pid,uid);}
    /*** 根据用户id和商品id生成随机盐* 1.检验用户合法性* 2.校验库存* 3.生成hashkey* 4.生成MD5** @param pid* @param uid* @return*/@Transactionalpublic String createMD5(Integer pid, Integer uid) {checkUser(uid);checkStock(pid);return createKey(pid, uid);}/*** 校验用户合法性*/public void checkUser(Integer id) {User user = userMapper.selectById(id);if (user == null) {throw new RuntimeException("用户不存在!");}}/*** 生成key,并存入redis** @param pid* @param uid* @return*/public String createKey(Integer pid, Integer uid) {String key = "SECOND_KILL" + pid + uid;String value = MD5Encoder.encode(key.getBytes());redisTemplate.opsForValue().set(key, value, 60, TimeUnit.SECONDS);return value;}

修改下单接口

    /*** 0.检验MD5* 1.验证库存* 2.修改库存* 3.创建订单** @param productId* @return*/@Transactionalpublic Order Qg(Integer productId, Integer uid, String md5) {checkMD5(productId, uid, md5);checkTime(productId);Stock stock = checkStock(productId);updateStock(stock);Order order = createOrder(stock);return order;}/*** 生成订单前校验MD5** @param uid* @param md5*/private void checkMD5(Integer pid, Integer uid, String md5) {if (!md5.equals(createMD5(pid, uid))) {throw new RuntimeException("参数非法!");}}

3.单用户接口调用频率限制

为了防止出现用户撸羊毛,限制用户的购买数量。

用redis给每个用户做访问统计,甚至带上商品id,对单个商品进行访问统计。

    /*** 用户点击抢购,开始下单*/@GetMapping("qg/{productId}/{uid}/{md5}")public String Qg(@PathVariable("productId") Integer productId,@PathVariable("uid")Integer uid,@PathVariable("md5")String md5) {if (!rateLimiter.tryAcquire(2, TimeUnit.SECONDS)){return "抢购失败,请重试!";}//查询用户是否抢购过该商品 ,并在用户下单成功后将该商品加入redisif (!userService.checkIsBuy(uid,productId)){return "已经购买过该商品,请勿重复下单!";}orderService.Qg(productId,uid,md5);return "抢购成功,耗时为:"+rateLimiter.acquire();}
    /*** 校验用户是否购买过该商品* @param uid* @param productId* @return*/public boolean checkIsBuy(Integer uid, Integer productId) {return redisTemplate.opsForHash().hasKey("SECOND_KILL_BUYED"+productId,uid);}
    /*** 0.检验MD5* 1.验证库存* 2.修改库存* 3.创建订单* 4.用户下单成功后将该商品加入redis** @param productId* @return*/@Transactionalpublic Order Qg(Integer productId, Integer uid, String md5) {checkMD5(productId, uid, md5);checkTime(productId);Stock stock = checkStock(productId);updateStock(stock);Order order = createOrder(stock);updateRedis(uid, productId);return order;}/*** 用户下单成功后将该商品加入redis*/private void updateRedis(Integer uid, Integer productId) {redisTemplate.opsForHash().increment("SECOND_KILL_BUYED"+productId,uid, 1);}

至此,单机的小体量秒杀系统基本结束,为什么说小体量?因为现在我们的合法购买请求完全打入到了数据库,对数据库压力过大,我们可以考虑操作redis缓存,使用消息队列实现异步下单支付。同时,如果体量再次升级,我们可以考虑使用集群,分布式,随之而来就产生了新的问题,分布式锁的解决。

单机秒杀系统的架构设计与实现相关推荐

  1. 7个不同的维度,详解秒杀系统的架构设计

    今天我从 7 个不同的维度,讲讲秒杀系统的架构设计,主要知识点如下: Nginx + 前后端分离 + CDN 缓存 + 网关(限流+熔断) 集群的路由层 + Redis(缓存热点数据.分布式锁) MQ ...

  2. Java编程详细解析—淘宝大秒杀系统是如何设计的?

    2019独角兽企业重金招聘Python工程师标准>>> 摘要 最初的秒杀系统的原型是淘宝详情上的定时上架功能,由于有些卖家为了吸引眼球,把价格压得很低.但这给的详情系统带来了很大压力 ...

  3. 小米的开源监控系统open-falcon架构设计,看完明白如何设计一个好的系统

    小米的开源监控系统open-falcon架构设计,看完明白如何设计一个好的系统 小米的http://book.open-falcon.org/zh/intro/ 早期,一直在用zabbix,不过随着业 ...

  4. 一个小型的网页抓取系统的架构设计

    一个小型的网页抓取系统的架构设计 网页抓取服务是互联网中的经常使用服务.在搜索引擎中spider(网页抓取爬虫)是必需的核心服务.搜索引擎的衡量指标"多.快.准.新"四个指标中,多 ...

  5. 新浪微博:大规模离线视频处理系统的架构设计

    微博视频平台在4亿月活用户吃瓜嗨聊的高并发.大流量背景下,既要保证用户微博生产和消费体验,又要支持业务快速迭代,确保正确性.稳定性和高可用性.本次演将以微博视频大规模视频离线处理系统的架构设计为主题为 ...

  6. Java生鲜电商平台-促销系统的架构设计与源码解析

    Java生鲜电商平台-促销系统的架构设计与源码解析 说明:本文重点讲解现在流行的促销方案以及源码解析,让大家对促销,纳新有一个深入的了解与学习过程. 促销系统是电商系统另外一个比较大,也是比较复杂的系 ...

  7. 汽车电子专业知识篇(三十二)-整车电控系统及架构设计技术

    本文的目的是基于我们对域控制设计方法的研究,提出相关的设计过程和规则,从而设计出我们3年后的新电控系统及架构平台,也就为实现软件定义汽车和硬件通用化提供可能性.同时,也希望能为国内电控系统及架构设计标 ...

  8. 如何进行系统的架构设计?

    一个软件项目在需求确定后,就可以开始系统的架构设计了.架构设计不同于编写代码,需要遵循严格的语法和编程规范.它没有规范可遵循,存在即合理,适合系统开发和运行的架构就是最合理的系统架构. 系统的架构设计 ...

  9. ADAS系统安全架构设计及安全等级的分解

    已剪辑自: https://mp.weixin.qq.com/s/PaFQDUR_iOnEeueYQ82m_w 笔者从事功能安全领域工作八年有余,结合个人经验分享一下对系统安全架构设计的理解,希望能够 ...

最新文章

  1. Java成员变量与类变量_Java基础随记2-成员变量和类变量的区别
  2. oracle xclock xmanager
  3. Java实用教程笔记 接口与实现
  4. elasticsearch6.x {error:Content-Type header [application/x-www-form-urlencoded] is not supported
  5. SpringCloud微服务架构之,Hystrix 熔断器,Gateway 网关
  6. SAP UI5 OData Json model name
  7. C++ MFC string转Cstring为什么会乱码
  8. Linux 常用命令随笔(二)
  9. 【简讯】ISO确定C++的升级
  10. csv处理数据后存入数据库
  11. web打印控件Lodop轻松输出清晰的图表和条码
  12. 华为商城抢购工具_华为套路太多,MATE40RS开启摇号模式,抽中资格仍需抢购
  13. vue全选和取消全选(无bug)
  14. 【转】java枚举类型ENUM
  15. python---之round
  16. Redhat8 配置使用阿里源(关闭官方订阅)
  17. win10不兼容 软件 ietest
  18. MySQL 数据库删除操作中的 delete、drop、 truncate 区别在哪?
  19. JAVA的stream流操作详细解析
  20. 让我们再聊聊TDD 续-正其思规其行

热门文章

  1. 《现代前端技术解析》第4-5章 阅读笔记
  2. 吐血整理!四年Java面试遇到的问题整理
  3. 布局技巧-等高布局 圣杯布局 双飞翼布局
  4. [转]《做第一等的事业,做中国的脊梁》 ——清华大学校长在本科生毕业典礼的讲话
  5. 腾讯高速增长初现天花板 微信或将成为未来门票
  6. js 比较时间大小.html
  7. 为了祭奠那永不瞑目的爱情
  8. 通达OA工作流数据报表二次开发版
  9. C++的generate函数
  10. 红外数据的基本原理和处理机制