首页 » 网站推广 » phprediskeyslimit技巧_若何设计并实现一个秒杀系统含完整代码

phprediskeyslimit技巧_若何设计并实现一个秒杀系统含完整代码

duote123 2024-11-21 0

扫一扫用手机浏览

文章目录 [+]

周一至周五早八点半!
佳构技能文章定时送上!


佳构学习资料获取通道,拜会文末

phprediskeyslimit技巧_若何设计并实现一个秒杀系统含完整代码

本文来源:crossoverJie

phprediskeyslimit技巧_若何设计并实现一个秒杀系统含完整代码
(图片来自网络侵删)

序言

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

本次采取循规蹈矩的办法逐步提高性能达到并发秒杀的效果,文章较长,请准备好瓜子板凳^_^

本文所有涉及的代码:

https://github.com/crossoverJie/SSMhttps://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 供应出接口):

@Servicepublic 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< span=\"大众\公众> 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 appnameappname=\公众consumer\"大众echo \"大众input=\公众$appnamePID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')# 遍历杀掉 pidfor var in ${PID[@]};do echo \"大众loop pid= $var\"大众 kill -9 $vardoneecho \"大众kill $appname success\"大众cd ..git pullcd SSM-SECONDS-KILLmvn -Dmaven.test.skip=true clean packageecho \"大众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/webappsecho \公众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/webappsecho \"大众cp tomcat-dubbo-consumer-7083-slave/webapps ok!\"大众sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/bin/startup.shecho \"大众tomcat-dubbo-consumer-8083/bin/startup.sh success\公众sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/bin/startup.shecho \"大众tomcat-dubbo-consumer-7083-slave/bin/startup.sh success\"大众echo \"大众start $appname success\"大众

构建 Service

# 构建做事供应者#read appnameappname=\"大众provider\"大众echo \"大众input=\公众$appnamePID=$(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# 遍历杀掉 pidfor var in ${PID[@]};do echo \公众loop pid= $var\公众 kill -9 $vardoneecho \公众kill $appname success\"大众cd ..git pullcd SSM-SECONDS-KILLmvn -Dmaven.test.skip=true clean packageecho \"大众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/webappsecho \"大众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/webappsecho \"大众cp tomcat-dubbo-provider-7080-slave/webapps ok!\公众sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/bin/startup.shecho \"大众tomcat-dubbo-provider-8080/bin/startup.sh success\公众sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/bin/startup.shecho \"大众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 如下:

@Configurationpublic 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 字的作文都憋不出来,可想而知是有多难得了。

以上内容欢迎谈论。

END

一大波微做事、分布式、高并发、高可用的原创系列文章正在路上,

欢迎关注头条号:石杉的架构条记

周一至周五早八点半!
佳构技能文章定时送上!


十余年BAT架构履历倾囊相授

标签:

相关文章

招商蛇口中国房地产龙头企业,未来可期

招商蛇口(股票代码:001979),作为中国房地产企业的领军企业,自成立以来始终秉持“以人为本,追求卓越”的经营理念,致力于打造高...

网站推广 2025-02-18 阅读1 评论0