网站首页 > 博客文章 正文
一个基于 Spring Boot 2.1 的电商秒杀系统。 让程序员能够轻松进阶 Java 高并发架构,很适合中小型互联网项目的电商秒杀,抢票等场景。采用最新的 Spring Boot, MyBatis 版本。
技术栈
- Spring Boot 2.X
- MyBatis
- Redis, MySQL
- Thymeleaf + Bootstrap
- RabbitMQ
- Zookeeper, Apache Curator
架构图
部署图 (zookeeper暂时没有用上, 忽略之)
秒杀过程
秒杀进行的过程包含两步骤: 步骤一(秒杀):在Redis里进行秒杀。 这个步骤用户并发量非常大,抢到后,给与30分钟的时间等待用户付款, 如果用户过期未付款,则Redis库存加1 ,算用户自动放弃付款。
步骤二(付款):用户付款成功后,后台把付款记录持久化到MySQL中,这个步骤并发量相对小一点,使用数据库的事务解决数据一致性问题
下面重点讲步骤一,秒杀过程
秒杀步骤流程图
1.流程图Step1:先经过Nginx负载均衡和分流
2.进入jseckill程序处理。 Google guava RateLimiter限流。 并发量大的时候,直接舍弃掉部分用户的请求
3.Redis判断是否秒杀过。避免重复秒杀。如果没有秒杀过
把用户名(这里是手机号)和seckillId封装成一条消息发送到RabbitMQ,请求变成被顺序串行处理
立即返回状态“排队中”到客户端上,客户端上回显示“排队中...”
4.后台监听RabbitMQ里消息,每次取一条消息,并解析后,请求Redis做库存减1操作(decr命令)
并手动ACK队列 如果减库存成功,则在Redis里记录下库存成功的用户手机号userPhone.
5.流程图Step2:客户端排队成功后,定时请求后台查询是否秒杀成功,后面会去查询Redis是否秒杀成功
如果抢购成功,或者抢购失败则停止定时查询, 如果是排队中,则继续定时查询。
1.总体架构
系统部署图
秒杀进行的过程包含两步骤: 步骤一(秒杀):在Redis里进行秒杀。 这个步骤用户并发量非常大,抢到后,给与30分钟的时间等待用户付款, 如果用户过期未付款,则Redis库存加1 ,算用户自动放弃付款。
步骤二(付款):用户付款成功后,后台把付款记录持久化到MySQL中,这个步骤并发量相对小一点,使用数据库的事务解决数据一致性问题
秒杀网站的静态资源,比如静态网页引用的js,css,图片,音频,视频等放到CDN(内容分发网络)上。
如果小型互联网公司为了减少成本,可以把静态资源部署到nginx下。利用nginx提供静态资源服务的高并发性能
的特点,可以最大可能的提高静态资源的访问速度。
通过nginx反向代理,对外只暴露80端口。同时配置nginx的负载均衡,为多个jseckill-backend集群节点提供
负载均衡。 负载均衡策略设置成按照几台应用服务器的性能大小的权重分配就行了。
MySQl部署采用Master-Slave主从复制方式来做读写分离, 提高数据库的高并发能力。
2.后端暴露秒杀接口
后端暴露接口的作用是:当秒杀时间开始后,才暴露每个商品的md5,只有拿到md5值,才能形成有效的秒杀请求.
秒杀时间段结束后,此接口不再返回md5值.
暴露秒杀接口数据,属于热点数据,并且值是不变的(库存量除外), 我们把它存在Redis上,Redis是基于内存的
非阻塞性多路复用,采用了epool技术,操作数据远远快于磁盘和数据库操作。
代码见SeckillServiceImpl.java的方法public Exposer exportSeckillUrl(long seckillId)
存Redis前,先用Protostuff框架把对Seckill对象序列化成二进制字节码
源码
@Override public Exposer exportSeckillUrl(long seckillId) { // 优化点:缓存优化:超时的基础上维护一致性 //1.访问Redis Seckill seckill = redisDAO.getSeckill(seckillId); if (seckill == null) { //2.访问数据库 seckill = seckillDAO.queryById(seckillId); if (seckill == null) { return new Exposer(false, seckillId); } else { //3.存入Redis redisDAO.putSeckill(seckill); } } Date startTime = seckill.getStartTime(); Date endTime = seckill.getEndTime(); //系统当前时间 Date nowTime = new Date(); if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) { return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); } //转化特定字符串的过程,不可逆 String md5 = getMD5(seckillId); return new Exposer(true, md5, seckillId); }
3.后端秒杀处理
3.1 Java后端限流
使用Google guava的RateLimiter来进行限流
例如:每秒钟只允许10个人进入秒杀步骤. (可能是拦截掉90%的用户请求,拦截后直接返回"很遗憾,没抢到")
AccessLimitServiceImpl.java代码
package com.liushaoming.jseckill.backend.service.impl; import com.google.common.util.concurrent.RateLimiter; import com.liushaoming.jseckill.backend.service.AccessLimitService; import org.springframework.stereotype.Service; /** * 秒杀前的限流. * 使用了Google guava的RateLimiter */ @Service public class AccessLimitServiceImpl implements AccessLimitService { /** * 每秒钟只发出10个令牌,拿到令牌的请求才可以进入秒杀过程 */ private RateLimiter seckillRateLimiter = RateLimiter.create(10); /** * 尝试获取令牌 * @return */ @Override public boolean tryAcquireSeckill() { return seckillRateLimiter.tryAcquire(); } }
使用限流, SeckillServiceImpl.java
@Override @Transactional /** * 执行秒杀 */ public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException { if (accessLimitService.tryAcquireSeckill()) { // 如果没有被限流器限制,则执行秒杀处理 return updateStock(seckillId, userPhone, md5); } else { //如果被限流器限制,直接抛出访问限制的异常 logger.info("--->ACCESS_LIMITED-->seckillId={},userPhone={}", seckillId, userPhone); throw new SeckillException(SeckillStateEnum.ACCESS_LIMIT); } }
3.2 Redis执行秒杀
秒杀步骤流程图
1.流程图Step1:先经过Nginx负载均衡和分流
2.进入jseckill程序处理。 Google guava RateLimiter限流。 并发量大的时候,直接舍弃掉部分用户的请求
3.Redis判断是否秒杀过。避免重复秒杀。如果没有秒杀过
把用户名(这里是手机号)和seckillId封装成一条消息发送到RabbitMQ,请求变成被顺序串行处理
立即返回状态“排队中”到客户端上,客户端上回显示“排队中...”
4.后台监听RabbitMQ里消息,每次取一条消息,并解析后,请求Redis做库存减1操作(decr命令)
并手动ACK队列 如果减库存成功,则在Redis里记录下库存成功的用户手机号userPhone.
5.流程图Step2:客户端排队成功后,定时请求后台查询是否秒杀成功,后面会去查询Redis是否秒杀成功
3.3 付款后减库存
源码见SeckillServiceImpl.java 原理是:
在public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
里,先insertSuccessKilled(),再reduceNumber()
先插入秒杀记录,再减库存。 这样行锁只作用于减库存一个阶段,提高了操作数据库的并发性能。
(否则如果先减库存,再插入秒杀记录,则update操作产生的行锁会持续整个事务时间阶段,性能差)
源码
@Override @Transactional /** * 先插入秒杀记录再减库存 */ public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { if (md5 == null || !md5.equals(getMD5(seckillId))) { logger.info("seckill data rewrite!!!. seckillId={},userPhone={}", seckillId, userPhone); throw new SeckillException("seckill data rewrite"); } //执行秒杀逻辑:减库存 + 记录购买行为 Date nowTime = new Date(); try { //插入秒杀记录(记录购买行为) int insertCount = successKilledDAO.insertSuccessKilled(seckillId, userPhone); //唯一:seckillId,userPhone if (insertCount <= 0) { //重复秒杀 logger.info("seckill repeated. seckillId={},userPhone={}", seckillId, userPhone); throw new RepeatKillException("seckill repeated"); } else { //减库存,热点商品竞争 // reduceNumber是update操作,开启作用在表seckill上的行锁 int updateCount = seckillDAO.reduceNumber(seckillId, nowTime); if (updateCount <= 0) { //没有更新到记录,秒杀结束,rollback throw new SeckillCloseException("seckill is closed"); } else { //秒杀成功 commit SuccessKilled payOrder = successKilledDAO.queryByIdWithSeckill(seckillId, userPhone); logger.info("seckill SUCCESS->>>. seckillId={},userPhone={}", seckillId, userPhone); //事务结束,关闭作用在表seckill上的行锁 return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, payOrder); } } } catch (SeckillCloseException e1) { throw e1; } catch (RepeatKillException e2) { throw e2; } catch (Exception e) { logger.error(e.getMessage(), e); // 所有编译期异常 转化为运行期异常 throw new SeckillException("seckill inner error:" + e.getMessage()); } }
4.集群的配置
- RabbitMQ的集群配置
#Rabbitmq配置 rabbitmq.address-list=192.168.20.3:5672,localhost:5672 rabbitmq.username=myname rabbitmq.password=somepass rabbitmq.publisher-confirms=true rabbitmq.virtual-host=/vh_test rabbitmq.queue=seckill
RabbitMQ的集群地址这样配置
rabbitmq.address-list=192.168.20.3:5672,localhost:5672
规则是每个地址采用host:port的格式,多个mq服务器地址采用英文的逗号隔开。中间不要有多余的空格
集群原理, 下面这个方法可以根据地址列表,来返回可用的MQ地址。 如果都不可用,则直接抛出异常。
com.rabbitmq.client.ConnectionFactory#newConnection(List<Address> addrs) throws IOException, TimeoutException {}
应用代码见com.liushaoming.jseckill.backend.config.MQConfig
代码片段
@Bean("mqConnectionSeckill") public Connection mqConnectionSeckill(@Autowired MQConfigBean mqConfigBean) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); //用户名 factory.setUsername(username); //密码 factory.setPassword(password); //虚拟主机路径(相当于数据库名) factory.setVirtualHost(virtualHost); //返回连接 return factory.newConnection(mqConfigBean.getAddressList()); }
猜你喜欢
- 2024-09-11 kubernetes基础知识之项目部署(k8s项目部署)
- 2024-09-11 聊聊kingbus的startMasterServer(聊聊日常电视剧全集免费)
- 2024-09-11 这篇Redis文章,图灵看了都说好(redis原理图)
- 2024-09-11 PT-KILL长尾慢SQL有时失灵?自写脚本更靠谱
- 2024-09-11 技术分享 | 从库 MTS 多线程并行回放(一)
- 2024-09-11 常用数据库的最大连接数的获取和修改
- 2024-09-11 MySQL 调试环境搭建:VSCode + Docker
- 2024-09-11 Linux服务器百万并发实现与问题排查
- 2024-09-11 Elasticsearch + Logstash + Kibana 安装(全)
- 2024-09-11 图文结合带你搞懂MySQL日志之Slow Query Log(慢查询日志)
你 发表评论:
欢迎- 06-23MySQL合集-mysql5.7及mysql8的一些特性
- 06-23MySQL CREATE TABLE 简单设计模板交流
- 06-23MYSQL表设计规范(mysql设计表注意事项)
- 06-23MySQL数据库入门(四)数据类型简介
- 06-23数据丢失?别慌!MySQL备份恢复攻略
- 06-23MySQL设计规范(mysql 设计)
- 06-23MySQL数据实时增量同步到Elasticsearch
- 06-23MySQL 避坑指南之隐式数据类型转换
- 最近发表
- 标签列表
-
- powershellfor (55)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)