您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

如何使用Java实现高效的秒杀系统?

2025/9/22 12:54:21发布33次查看
首先来看看最终架构图:
先简单根据这个图谈下请求的流转,因为后面不管怎么改进,这些都是不变的:
前端请求进入 web 层,对应的代码就是 controller。
之后将真正的库存校验、下单等请求发往 service 层,其中 rpc 调用依然采用的 dubbo,只是更新为***版本。
service 层再对数据进行落地,下单完成。
***制
抛开秒杀这个场景来说,正常的一个下单流程可以简单分为以下几步:
校验库存
扣库存
创建订单
支付
基于上文的架构,我们有了以下实现,先看看实际项目的结构:
还是和以前一样:
提供出一个 api 用于 service 层实现,以及 web 层消费。
web 层简单来说就是一个 spring mvc。
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:
<update id="updatebyoptimistic" parametertype="com.crossoverjie.seconds.kill.pojo.stock">        update stock        <set>            sale = sale + 1,            version = version + 1,        </set>         where id = #{id,jdbctype=integer}        and version = #{version,jdbctype=integer}     </update>
同样的测试条件,我们再进行上面的测试 /createoptimisticorder/1:
这次发现无论是库存订单都是 ok 的。
查看日志发现:
很多并发请求会响应错误,这就达到了效果。
提高吞吐量
为了进一步提高秒杀时的吞吐量以及响应效率,这里的 web 和 service 都进行了横向扩展:
web 利用 nginx 进行负载。
service 也是多台应用。
再用 jmeter 测试时可以直观的看到效果。
由于我是在阿里云的一台小水管服务器进行测试的,加上配置不高、应用都在同一台,所以并没有完全体现出性能上的优势( nginx  做负载转发时候也会增加额外的网络消耗)。
shell 脚本实现简单的 ci
由于应用多台部署之后,手动发版测试的痛苦相信经历过的都有体会。
这次并没有精力去搭建完整的 cicd,只是写了一个简单的脚本实现了自动化部署,希望给这方面没有经验的同学带来一点启发。
构建 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
因为加上该组件之后所有的请求都会经过 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 端:
/**      * 乐观锁更新库存 限流      * @param sid      * @return      */     @springcontrollerlimit(errorcode = 200)     @requestmapping(/createoptimisticlimitorder/{sid})     @responsebody     public string createoptimisticlimitorder(@pathvariable int sid) {         logger.info(sid=[{}], sid);         int id = 0;         try {             id = orderservice.createoptimisticorder(sid);         } catch (exception e) {             logger.error(exception,e);         }         return string.valueof(id);     }
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 层的逻辑重写了一遍,不过采用的是 spring boot。
以上就是如何使用java实现高效的秒杀系统?的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product