参考资料:
[1]. 秒杀系统基础版
[2]. 电商秒杀高级版
使用数据库需要引入的库
分别是MySQL对Jdbc的支持,阿里巴巴的连接池Druid
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
层次
模型
异常处理
一种处理方法是在Controller中定义方法ExceptionHandler,但是这种方法无法处理没有进入Controller的异常,比如404,这个时候可以用GlobalExceptionHandler.
ExceptionHandler的使用如下,它会在当前Controller抛出异常的时候进入这里面进行处理,如果没有ExceptionHandler,异常会直接抛给Tomcat,@ResponseStatus(HttpStatus.OK)是为了HTTP返回200。
//定义exceptionhandler解决未被controller层吸收的exception
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Object handlerException(HttpServletRequest request, Exception ex){
Map<String,Object> responseData = new HashMap<>();
if( ex instanceof BusinessException){
BusinessException businessException = (BusinessException)ex;
responseData.put("errCode",businessException.getErrCode());
responseData.put("errMsg",businessException.getErrMsg());
}else{
responseData.put("errCode", EmBusinessError.UNKNOWN_ERROR.getErrCode());
responseData.put("errMsg",EmBusinessError.UNKNOWN_ERROR.getErrMsg());
}
return CommonReturnType.create(responseData,"fail");
}
订单号信息
16位 = 8位时间信息(年月日)+中间6位(自增序列)+最后两位为分库分表位
自增序列需要新建Sequence表,有currentValue和getStep两个字段,每次获取的时候要用for update锁定,然后再设置currentValue为currentValue+step。
并且要解决两个问题:
1、currentValue超过6位
2、下单操作回滚的时候currentValue不应该回滚,不然增加到一定时候会重复,无法保证全局唯一性。
解决第二个问题需要在修改读取自增序列的方法上的@Transactional的propagation设置为Propagation.REQUIRED_NEW,
基础版之后的问题
image.png如何发现容量问题?
ps -ef | grep java 得到部署的线程数
pstree -p xxx | wc -l 进程的线程数量
top -H 任务管理器
load average 表示的是CPU的负载,包含的信息不是CPU的使用率状况,而是在一段时间内CPU正在处理以及等待CPU处理的进程数之和的统计信息,也就是CPU使用队列的长度的统计信息。
-
解决方案1——增加最大线程数
调大max-threads=800(4核8G的经验值),min-spare-threads=100(突然增加请求的时候有用),TPS从40+增加到200+
- 解决方案2——KeepAlive
keepAliveTimeOut:多少毫秒后不响应的断开keepalive
maxKeepAliveRequests:多少请求后keepalive断开失效
@Component
public class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
((TomcatServletWebServerFactory)factory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
@Override
public void customize(Connector connector) {
Http11NioProtocol protocolHandler = (Http11NioProtocol) connector.getProtocolHandler();
protocolHandler.setKeepAliveTimeout(30*1000);
protocolHandler.setMaxKeepAliveRequests(10000);
protocolHandler.setMaxConnections(5);
}
});
}
}
并不是线程数量越多越好,太多会导致线程切换耗时太长。
如何使得系统水平拓展
-
解决方案1
数据库服务器:TPS=1500(跟前面比,带宽增加了),平均耗时500ms
NGINX:两台后端服务器,TPS=1700
-
nginx与后端服务器要保持长连接
整条链路都要保持长连接。
nginx高性能的原因
-
epoll多路复用
阻塞->select(循环遍历检查)->epoll
-
master worker进程模型
-
协程
一个进程对应一个协程,并发并且不用上下文切换。
QPS和TPS
QPS:Query Per Second
TPS:Transactions Per Second
一般的,评价系统性能均以每秒钟完成的技术交易的数量来衡量。系统整体处理能力取决于处理能力最低模块的TPS值。TPS指的是服务器每秒能够处理的事务数,而QPS是针对一个查询,服务器的每秒响应数
nginx反向代理负载均衡(分布式拓展)
- nginx作为web服务器
web静态资源,css,js,html - nginx作为动静分离服务器
-
nginx作为反向代理服务器
nginx怎么知道哪个时候反向代理,什么时候自己发送静态资源?
路径
查询性能优化之多级缓存
-
redis缓存
单机模式VS哨兵(sentinal)模式:哨兵负责选出master和slave,用心跳机制监控master和slave,如果master挂掉了,将slave改为master,然后哨兵会通知java程序,master已经改变了,
集群cluster模式:多台redis机器都可以互相感知到对方的存在,用户只需对其中一台进去ask即可获得全部的机器列表,省去管理集群的工作。
redis存储的对象需要实现seariable接口
- 本地热点缓存
使用redis之后TPS=2000左右,平均耗时降低到200ms左右。
使用本地缓存(guava)之后TPS=3000左右,平均耗时40ms左右。
redis需要通过网络。
热点数据(因为内存有限)
脏读非常不敏感(如果数据修改的话,redis可以设置key失效)
内存可控
ConcurrentHashMap没有失效机制,加上用的是锁的机制,会影响并发性。
Guava cache本质是一个HashMap,可控制大小和超时时间,可配置lru策略。
缺点:不能更新,大小有限。
nginx proxy cache缓存
nginx lua缓存
交易性能瓶颈
jmeter压测:TPS=200,平均耗时500ms
下单交易:6次数据库IO,减库存的时候有行锁
加上redis缓存之后:TPS=1000,平时耗时600+
- 优化库存行锁
-
给库存表的item_id增加索引
这样就不会锁定整个表,只会锁定行 -
扣减库存缓存化
解决:活动发布的时候将数据放到redis
问题:数据库记录不一致(redis可能会奔溃) -
异步同步数据库
解决:用异步消息扣减数据库库存,rocketmq基于kafka进行开发的
问题:异步消息发送失败,(接收消息后)扣减库存失败怎么处理,下单失败无法正确补回库存。 -
减缓存库存成功,但是后面订单入库失败无法将缓存和数据库的库存加回来
解决:在下单事务最后面再发送异步消息
问题:commit可能失败,那么库存还是会减掉
解决:在事务成功提交后再发送减库存的异步消息
问题:发送消息可能失败
解决:TransactionMQProducer,跟数据库事务一样,成功才发送消息,失败了不发送消息。也就是消息还是会发送,但是是prepared状态,同时会执行整个创建订单的操作(可能会很久),然后返回对应的状态,失败的话,消息会被回滚。
问题:下单状态可能未知
解决:库存操作流水。数据库里面的数据分为主业务数据和操作型数据,库存操作流水就是一种操作型数据。数据库流水在开始下单之前设置为初始状态,下单的最后设置为完成状态,中间的时候,事务消息异步检查的时候可以得到数据库的状态。现在下订单的操作很多都移动到redis缓存中去了,除了订单入库,还有最后的库存操作流水,但是最后这个对于的行锁只是针对每个操作单独的行锁,性能并不能下降太多。
问题:redis不可用,扣减流水
-
redis不可用
解决:设计原则为:宁可少买,不多卖。那么redis不可用的时候,不能追溯到数据库,因为数据库的减库存是异步的,可能之前的还没有减掉。宁愿redis比数据库中的数据少。 -
售罄:一开始就下了数据库流水,如果这个时候还没已经没货了,会有很大数据库流水。
解决:用redis设置一个key-value设置售罄标志。 -
销量逻辑异步化,交易单逻辑异步化(减库存成功后直接将后面的操作异步)
- 库存数据库最终一致性保证
页面静态化
活动缓存库存
每次下单的时候减少的操作是发生在redis缓存,同时用消息中间件rocketmq进行MySQL数据库的减少,不如redis崩了,数据就没了。
rocketmq角色
生产者:产生消息放到消息队列。
消费者:获取消息队列的消息。
消费组:对应一个主题,多台主机共同负责某个主题的消息的消费,每条消息只能被其中一台主机消费。不同的消费组可以共同且不互斥地消费同一个主题的消息。
NameServer跟zookeeper一样,用来做生产者,消费者,消息队列的中介,负责注册和发现,还有消费队列的master的选举。
Broker:一个主题的机器,对应多个队列,但是可能会有多个备份(slave),slave只备份,不发送消息。
分布式事务的可用性和一致性
两个是矛盾,要保证一致性,那么只能稍微牺牲可用性,比如等数据完全同步后再开放使用。现实中,我们肯定要一致性,但是我们先保证可用性,一致性稍微完全同步,最后完全同步。
网友评论