点击上方“方志朋”,选择“设为星标”

回复”666“获取新整理的面试资料

本文来源:crossoverJie

前言

之前在 Java-Interview 中提到过秒杀架构的设计,这次基于其中的理论简单实现了一下。

本次采用循序渐进的方式逐步提高性能达到并发秒杀的效果,文章较长,请准备好瓜子板凳^_^

本文所有涉及的代码:

  • https://github.com/crossoverJie/SSM

  • https://github.com/crossoverJie/distributed-redis-tool

首先来看看最终架构图:

先简单根据这个图谈下请求的流转,因为后面不管怎么改进这个都是没有变的。

  • 前端请求进入 web 层,对应的代码就是 controller

  • 之后将真正的库存校验、下单等请求发往 Service 层(其中 RPC 调用依然采用的 dubbo,只是更新为最新版本,本次不会过多讨论 dubbo 相关的细节,有兴趣的可以查看 基于dubbo 的分布式架构)

  • Service 层再对数据进行落地,下单完成

无限制

其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步:

  • 校验库存

  • 扣库存

  • 创建订单

  • 支付

基于上文的架构所以我们有了以下实现:

先看看实际项目的结构:

还是和以前一样:

  • 提供出一个 API 用于 Service 层实现,以及 web 层消费。

  • web 层简单来说就是一个 SpringMVC

  • Service 层则是真正的数据落地。

  • SSM-SECONDS-KILL-ORDER-CONSUMER 则是后文会提到的 Kafka 消费。

数据库也是只有简单的两张表模拟下单:

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 AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

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 AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;

web 层 controller 实现:

   @Autowired

   private StockService stockService;

   @Autowired

   private OrderService orderService;

   @RequestMapping("/createWrongOrder/{sid}")

   @ResponseBody

   public String createWrongOrder(@PathVariable int sid) {

       logger.info("sid=[{}]", sid);

       int id = 0;

       try {

           id = orderService.createWrongOrder(sid);

       } catch (Exception e) {

           logger.error("Exception",e);

       }

       return String.valueOf(id);

   }

其中 web 作为一个消费者调用看 OrderService 提供出来的 dubbo 服务。

Service 层, OrderService 实现:

首先是对 API 的实现(会在 API 提供出接口):

@Service

public class OrderServiceImpl implements OrderService {

   @Resource(name = "DBOrderService")

   private com.crossoverJie.seconds.kill.service.OrderService orderService ;

   @Override

   public int createWrongOrder(int sid) throws Exception {

       return orderService.createWrongOrder(sid);

   }

}

这里只是简单调用了 DBOrderService 中的实现,DBOrderService 才是真正的数据落地,也就是写数据库了。

DBOrderService 实现:

Transactional(rollbackFor = Exception.class)

@Service(value = "DBOrderService")

public class OrderServiceImpl implements OrderService {

   @Resource(name = "DBStockService")

   private com.crossoverJie.seconds.kill.service.StockService stockService;

   @Autowired

   private StockOrderMapper orderMapper;

   @Override

   public int createWrongOrder(int sid) throws Exception{

       //校验库存

       Stock stock = checkStock(sid);

       //扣库存

       saleStock(stock);

       //创建订单

       int id = createOrder(stock);

       return id;

   }

   private Stock checkStock(int sid) {

       Stock stock = stockService.getStockById(sid);

       if (stock.getSale().equals(stock.getCount())) {

           throw new RuntimeException("库存不足");

       }

       return stock;

   }

   private int saleStock(Stock stock) {

       stock.setSale(stock.getSale() + 1);

       return stockService.updateStockById(stock);

   }

   private int createOrder(Stock stock) {

       StockOrder order = new StockOrder();

       order.setSid(stock.getId());

       order.setName(stock.getName());

       int id = orderMapper.insertSelective(order);

       return id;

   }        

}

预先初始化了 10 条库存。

手动调用下 createWrongOrder/1 接口发现:

库存表:

订单表:

一切看起来都没有问题,数据也正常。但是当用 JMeter 并发测试时:

测试配置是:300个线程并发,测试两轮来看看数据库中的结果:

请求都响应成功,库存确实也扣完了,但是订单却生成了 124 条记录。这显然是典型的超卖现象。

其实现在再去手动调用接口会返回库存不足,但为时晚矣。

乐观锁更新

怎么来避免上述的现象呢?最简单的做法自然是乐观锁了,来看看具体实现:

其实其他的都没怎么改,主要是 Service 层。

   @Override

   public int createOptimisticOrder(int sid) throws Exception {

       //校验库存

       Stock stock = checkStock(sid);

       //乐观锁更新库存

       saleStockOptimistic(stock);

       //创建订单

       int id = createOrder(stock);

       return id;

   }

   private void saleStockOptimistic(Stock stock) {

       int count = stockService.updateStockByOptimistic(stock);

       if (count == 0){

           throw new RuntimeException("并发更新库存失败") ;

       }

   }

对应的 XML:

    id="updateByOptimistic" parameterType="com.crossoverJie.seconds.kill.pojo.Stock">

       update stock

           sale = sale + 1,

           version = version + 1,

       WHERE id = #{id,jdbcType=INTEGER}

       AND version = #{version,jdbcType=INTEGER}

同样的测试条件,我们再进行上面的测试 /createOptimisticOrder/1

这次发现无论是库存订单都是 OK 的。

查看日志发现:

很多并发请求会响应错误,这就达到了效果。

提高吞吐量

为了进一步提高秒杀时的吞吐量以及响应效率,这里的 web 和 Service 都进行了横向扩展。

  • web 利用 Nginx 进行负载。

  • Service 也是多台应用。

再用 JMeter 测试时可以直观的看到效果。

由于我是在阿里云的一台小水管服务器进行测试的,加上配置不高、应用都在同一台,所以并没有完全体现出性能上的优势( Nginx 做负载转发时候也会增加额外的网络消耗)。

shell 脚本实现简单的 CI

由于应用多台部署之后,手动发版测试的痛苦相信经历过的都有体会。

这次并没有精力去搭建完整的 CI CD,只是写了一个简单的脚本实现了自动化部署,希望对这方面没有经验的同学带来一点启发:

构建 web

#!/bin/bash

# 构建 web 消费者

#read appname

appname="consumer"

echo "input="$appname

PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')

# 遍历杀掉 pid

for var in ${PID[@]};

do

   echo "loop pid= $var"

   kill -9 $var

done

echo "kill $appname success"

cd ..

git pull

cd SSM-SECONDS-KILL

mvn -Dmaven.test.skip=true clean package

echo "build war success"

cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/webapps

echo "cp tomcat-dubbo-consumer-8083/webapps ok!"

cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/webapps

echo "cp tomcat-dubbo-consumer-7083-slave/webapps ok!"

sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/bin/startup.sh

echo "tomcat-dubbo-consumer-8083/bin/startup.sh success"

sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/bin/startup.sh

echo "tomcat-dubbo-consumer-7083-slave/bin/startup.sh success"

echo "start $appname success"

构建 Service

# 构建服务提供者

#read appname

appname="provider"

echo "input="$appname

PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')

#if [ $? -eq 0 ]; then

#    echo "process id:$PID"

#else

#    echo "process $appname not exit"

#    exit

#fi

# 遍历杀掉 pid

for var in ${PID[@]};

do

   echo "loop pid= $var"

   kill -9 $var

done

echo "kill $appname success"

cd ..

git pull

cd SSM-SECONDS-KILL

mvn -Dmaven.test.skip=true clean package

echo "build war success"

cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/webapps

echo "cp tomcat-dubbo-provider-8080/webapps ok!"

cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/webapps

echo "cp tomcat-dubbo-provider-7080-slave/webapps ok!"

sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/bin/startup.sh

echo "tomcat-dubbo-provider-8080/bin/startup.sh success"

sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/bin/startup.sh

echo "tomcat-dubbo-provider-8080/bin/startup.sh success"

echo "start $appname success"

之后每当我有更新,只需要执行这两个脚本就可以帮我自动构建。都是最基础的 Linux 命令,相信大家都看得明白。

乐观锁更新 + 分布式限流

上文的结果看似没有问题,其实还差得远呢。这里只是模拟了 300 个并发没有问题,但是当请求达到了 3000 ,3W,300W 呢?

虽说可以横向扩展可以支撑更多的请求,但是能不能利用最少的资源解决问题呢?其实仔细分析下会发现:

假设我的商品一共只有 10 个库存,那么无论你多少人来买其实最终也最多只有 10 人可以下单成功。

所以其中会有 99% 的请求都是无效的。

大家都知道:大多数应用数据库都是压倒骆驼的最后一根稻草。通过 Druid 的监控来看看之前请求数据库的情况:

因为 Service 是两个应用。 

数据库也有 20 多个连接。

怎么样来优化呢? 其实很容易想到的就是分布式限流。我们将并发控制在一个可控的范围之内,然后快速失败这样就能最大程度的保护系统。

distributed-redis-tool ⬆️v1.0.3

为此还对 https://github.com/crossoverJie/distributed-redis-tool 进行了小小的升级。

因为加上该组件之后所有的请求都会经过 Redis,所以对 Redis 资源的使用也是要非常小心。

API 更新

修改之后的 API 如下:

@Configuration

public class RedisLimitConfig {

   private Logger logger = LoggerFactory.getLogger(RedisLimitConfig.class);

   @Value("${redis.limit}")

   private int limit;

   @Autowired

   private JedisConnectionFactory jedisConnectionFactory;

   @Bean

   public RedisLimit build() {

       RedisLimit redisLimit = new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE)

               .limit(limit)

               .build();

       return redisLimit;

   }

}

这里构建器改用了 JedisConnectionFactory,所以得配合 Spring 来一起使用。

并在初始化时显示传入 Redis 是以集群方式部署还是单机(强烈建议集群,限流之后对 Redis 还是有一定的压力)。

限流实现

既然 API 更新了,实现自然也要修改:

   /**

    * limit traffic

    * @return if true

    */

   public boolean limit() {

       //get connection

       Object connection = getConnection();

       Object result = limitRequest(connection);

       if (FAIL_CODE != (Long) result) {

           return true;

       } else {

           return false;

       }

   }

   private Object limitRequest(Object connection) {

       Object result = null;

       String key = String.valueOf(System.currentTimeMillis() / 1000);

       if (connection instanceof Jedis){

           result = ((Jedis)connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));

           ((Jedis) connection).close();

       }else {

           result = ((JedisCluster) connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));

           try {

               ((JedisCluster) connection).close();

           } catch (IOException e) {

               logger.error("IOException",e);

           }

       }

       return result;

   }

   private Object getConnection() {

       Object connection ;

       if (type == RedisToolsConstant.SINGLE){

           RedisConnection redisConnection = jedisConnectionFactory.getConnection();

           connection = redisConnection.getNativeConnection();

       }else {

           RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection();

           connection = clusterConnection.getNativeConnection() ;

       }

       return connection;

   }

如果是原生的 Spring 应用得采用 @SpringControllerLimit(errorCode=200)注解。

实际使用如下,web 端:

Service 端就没什么更新了,依然是采用的乐观锁更新数据库。

再压测看下效果 /createOptimisticLimitOrderByRedis/1

首先是看结果没有问题,再看数据库连接以及并发请求数都有明显的下降

乐观锁更新 + 分布式限流 + Redis 缓存

其实仔细观察 Druid 监控数据发现这个 SQL 被多次查询:

其实这是实时查询库存的 SQL,主要是为了在每次下单之前判断是否还有库存。

这也是个优化点

这种数据我们完全可以放在内存中,效率比在数据库要高很多。

由于我们的应用是分布式的,所以堆内缓存显然不合适,Redis 就非常适合。

这次主要改造的是 Service 层:

  • 每次查询库存时走 Redis。

  • 扣库存时更新 Redis。

  • 需要提前将库存信息写入 Redis(手动或者程序自动都可以)。

主要代码如下:

   @Override

   public int createOptimisticOrderUseRedis(int sid) throws Exception {

       //检验库存,从 Redis 获取

       Stock stock = checkStockByRedis(sid);

       //乐观锁更新库存 以及更新 Redis

       saleStockOptimisticByRedis(stock);

       //创建订单

       int id = createOrder(stock);

       return id ;

   }

   private Stock checkStockByRedis(int sid) throws Exception {

       Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));

       Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));

       if (count.equals(sale)){

           throw new RuntimeException("库存不足 Redis currentCount=" + sale);

       }

       Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid));

       Stock stock = new Stock() ;

       stock.setId(sid);

       stock.setCount(count);

       stock.setSale(sale);

       stock.setVersion(version);

       return stock;

   }    

   /**

    * 乐观锁更新数据库 还要更新 Redis

    * @param stock

    */

   private void saleStockOptimisticByRedis(Stock stock) {

       int count = stockService.updateStockByOptimistic(stock);

       if (count == 0){

           throw new RuntimeException("并发更新库存失败") ;

       }

       //自增

       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(),1) ;

       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(),1) ;

   }    

压测看看实际效果 /createOptimisticLimitOrderByRedis/1

最后发现数据没问题,数据库的请求与并发也都下来了。

乐观锁更新 + 分布式限流 + Redis 缓存 + Kafka 异步

最后的优化还是想如何来再次提高吞吐量以及性能的。我们上文所有例子其实都是同步请求,完全可以利用同步转异步来提高性能啊。

这里我们将写订单以及更新库存的操作进行异步化,利用 Kafka 来进行解耦和队列的作用。

每当一个请求通过了限流到达了 Service 层通过了库存校验之后就将订单信息发给 Kafka ,这样一个请求就可以直接返回了。

消费程序再对数据进行入库落地。因为异步了,所以最终需要采取回调或者是其他提醒的方式提醒用户购买完成。

这里代码较多就不贴了,消费程序其实就是把之前的 Service 层的逻辑重写了一遍,不过采用的是 SpringBoot。

感兴趣的朋友可以看下:

https://github.com/crossoverJie/SSM/tree/master/SSM-SECONDS-KILL/SSM-SECONDS-KILL-ORDER-CONSUMER

总结

其实经过上面的一顿优化总结起来无非就是以下几点:

  • 尽量将请求拦截在上游。

  • 还可以根据 UID 进行限流。

  • 最大程度的减少请求落到 DB。

  • 多利用缓存。

  • 同步操作异步化。

  • fail fast,尽早失败,保护应用。

码字不易,这应该是我写过字数最多的了,想想当年高中 800 字的作文都憋不出来?,可想而知是有多难得了。

热门内容:     

  • 8种常见SQL错误用法

  • 分布式 id 生成器

  • Java 程序员必须清楚的 7 个性能指标

  • 阿里巴巴的技术专家,是如何画好架构图的?

  • 史上最烂的项目:苦撑 12 年,600 多万行代码

  • 一次 Jar 包升级引发的血案 & 解决

  • 如何优雅的导出 Excel

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

++实现 kill_如何设计并实现一个秒杀系统?(含完整代码)相关推荐

  1. 如何设计并实现一个秒杀系统?(含完整代码)

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 本文来源:crossoverJie 前言 之前在 Java-Int ...

  2. 面试必备:一个秒杀系统的设计思考

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:阿哲 segmentfault.com/a/11900000 ...

  3. 一个秒杀系统的设计思考

    简介:从架构视角来看,秒杀系统本质是一个高性能.高一致.高可用的三高系统.而打造并维护一个超大流量的秒杀系统需要进行哪些关注,就是本文讨论的话题. 秒杀大家都不陌生.自2011年首次出现以来,无论是双 ...

  4. 阿里最后一面,高并发下如何设计一个秒杀系统?

    近年来,随着"双十一"购物节和抖音等直播平台带货的热潮,大批促销活动涌现,「秒杀」这个词也越来越频繁地出现在我们的生活里. 除了那些头部的电商公司,某宝.某东,还有各种街.某说.某 ...

  5. 从0到1设计一个秒杀系统

    秒杀大家都不陌生.自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见.简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程.从架构视角来看,秒杀系统本 ...

  6. 经验:一个秒杀系统的设计思考

    点击上方「蓝字」关注我们 前言 秒杀大家都不陌生.自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见.简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的 ...

  7. 面试官:让你实现一个秒杀系统,你会怎么设计?

    秒杀大家都不陌生.自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见.简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程.从架构视角来看,秒杀系统本 ...

  8. 大厂最后一面,如何设计一个秒杀系统

    近年来,随着"双十一"购物节和抖音等直播平台带货的热潮,大批促销活动涌现,「秒杀」这个词也越来越频繁地出现在我们的生活里. 除了那些头部的电商公司,某宝.某东,还有各种街.某说.某 ...

  9. 一个秒杀系统的设计思考(整体思考,收藏了)

    作者:阿哲 来源:https://segmentfault.com/a/1190000020970562 前言 秒杀大家都不陌生.自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀 ...

最新文章

  1. iOS开发 - 获取时间段
  2. ExtJS4.2:自定义主题 入门
  3. InstallShield使用——菜单部分
  4. 监控软件nagios错误总结
  5. 盛大游戏回归A股进入倒计时:估值近300亿 腾讯年初曾入股
  6. 代码内查找函数引用_叮~~二级操作题 excel常考函数大梳理
  7. [Leedcode][JAVA][第136题][第137题][只出现一次的数字][位运算][HashSet][HashMap]
  8. Linux进阶之路————scp指令介绍与演示
  9. 智能家居制作之WiFi遥控家中设备
  10. mate30 e pro和mate30 pro的区别_新出的华为mate40跟华为mate40pro手机有何区别呢?
  11. python DataFrame获取行数、列数、索引及第几行第几列的值
  12. EL表达式判断条件要写在${}内
  13. 别了MongoDB?
  14. 场景,生态之面!——保险科技生态建设
  15. matlab nastran,nastran op2 matlab
  16. 对股票进行可视化分析
  17. 《微信公众号-腾讯问卷》02-如何在公众号中添加链接
  18. 自动登录163邮箱发送邮件(Python+Selenium)
  19. 人工智能+建筑,会产生什么?
  20. 4个好用的WebShell网站后门查杀工具 在线木马查杀

热门文章

  1. python画图武汉加油-python实现“武汉加油”点阵字
  2. python怎么用excel-Python怎么写入excel文件?详细实例在这里。。。
  3. python代码怎么运行-python代码是怎样运行的
  4. php和python学哪个-php和python哪个
  5. python简单代码演示效果-演示python如何创建和使用一个简单的元类的代码
  6. python在财务上的应用-Python用于财务工作培训
  7. python while循环语句-Python
  8. 北京python培训班价格-北京Python启蒙班
  9. xmind快速上手使用教程,提高工作效率
  10. 图片的批量下载 和 爬虫爬取图片数据集