1、解决分布式Session问题,服务器水平扩展时,多台服务器都可以响应。
2、多级缓存提高页面访问速度和并发量,减少数据库压力。利用内存标记减少redis的访问。
3、秒杀令牌对下单接口解耦,并限制令牌个数(创建秒杀大闸),一定程度缓解下单接口访问压力。
4、对库存做内存标记,减少redis压力。只在redis中更新库存,是以乐观锁方式。使用RocketMq消息队列异步同步数据库,达到一种柔性事务,减少数据库访问压力。并且防止消息发送失败导致数据不一致性,使用事务型消息包裹创建订单操作,保证createorder(1、订单入库;2、落单减库存;3、更新交易日志。)为原子性操作,也保证消息成功投放,即使发送失败也会回滚。
5、对开启事务型消息这个大事务和初始化订单流水,使用异步化操作,并且使用队列化泄洪。创建线程池,使用阻塞队列存储任务,如果我们希望所有任务都完成可以设置饱和策略为:Caller-Runs让主线程也帮助执行任务。使用Future对象异步接收执行结果。
6、md5密码校验,秒杀接口地址的隐藏,接口限流防刷,下单验证码手动延迟请求。
7、前后端分离,客户端使用token进行其他业务请求,减轻服务端检验用户的压力。
1、 分布式session问题,由于水平扩展不同服务器上tomcat容器各自存储session,如果ngnix将访问ServerA的前端请求,发送给ServerB,那么就会查不到对应session,session信息丢失,使得用户重新跳转到登录界面。集中存储到redis服务器上,建立的session信息都可以缓存中拿到。
2、多级缓存提高QPS
本项目大量的利用了缓存技术,包括用户信息缓存(分布式session),商品信息的缓存,商品库存缓存,订单的缓存,页面缓存。访问速度:内存>redis>数据库sql。
3、使用秒杀令牌token,将大量验证信息从下单接口分离出来,每个秒杀都需要创建特定的秒杀令牌PromoId_itemId_userId并限定10min存活时间,一定程度防止生成过多token,导致内存占满。限制令牌个数也能够防止恶意访问接口。
4、(1)库存售罄没必要再去访问redis,直接stock的内存标记。hashmap或者guava。(2)落单减库存是先扣减,不够再回补的一种乐观锁方式。数据库sql采用where stock>0方式加上行锁。并发时会导致QPS很低,性能大幅降低。所以使用消息队列做异步更新,降低数据库压力。(使用CAP理论,BASE理论,考虑可用性和分布式情况,牺牲即刻的一致性)分布式情况下,先扣减redis导致不一致性,但是能达到最终一致性的一种柔性事务。(3)消息发送失败可能导致数据不一致性(redis更新了,数据库没更新成功),需要用事务型消息包裹创建订单操作,保证createorder(1、订单入库;2、落单减库存;3、更新交易日志。)为原子性操作,也保证消息成功投放,即使发送失败也会回滚。但是生成的交易单号要设为Propagation.REQUIRES_NEW防止重复利用订单号。也就是处于大事务中只要完成了就会更新数据。
5、简单使用线程池等待队列方式进行队列泄洪,然后使用Future对象异步接收结果。
6、使用令牌桶算法限流,使用Google的RateLimiter。
7、优化redis中的key命名。方案:通用缓存key封装大量的缓存引用也出现了一个问题,如何识别不同模块中的缓存(key值重复,如何辨别是不同模块的key)
解决:利用一个抽象类,定义BaseKey(前缀),在里面定义缓存key的前缀以及缓存的过期时间从而实现将缓存的key进行封装。让不同模块继承它,这样每次存入一个模块的缓存的时候,加上这个缓存特定的前缀,以及可以统一制定不同的过期时间。
什么是Token?
Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。
Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位
部署shell脚本命令:
vim deploy.sh
nohup java -Xms400m -Xmx400m -XX:NewSize=200m -XX:MaxNewSize=200m -jar miaosha.jar —spring.config.addtion-location=/var/www/miaosha/application.properties
./deploy.sh &
1、设置jvm大小,初始400m,最大400m。新生代初始大小200m,最大200m。
2、项目打包成miaosha.jar,加上外挂配置文件application.properties会比项目中配置文件优先使用。
3、nohup加在命令前表示不挂断的运行命令。
4、**&**加在命令最后面,表示这个命令放在后台执行。
1、top -H查看服务器性能。cpu利用率(us用户空间占有率、sy表示内核空间占有率pthread)、load average最近5分钟cpu占有(负载高表示耗时工作多)。
2、pstree查看线程树,共有多少个线程并发。
用法:
pstree -p `ps -e | grep java | awk '{print $1}'` | wc -l
或
pstree -p 3660 | wc -l
例、查询当前整个系统已用的线程或进程数
pstree -p | wc -l
1、ps命令:Process Status缩写
参数含义:
-e 显示所有进程
-f 全格式
ps -ef 以全格式显示所有进程
2、“|”管道命令,需要接收前一个命令的输出,作为下一个命令的输入,可用于在命令行中执行多条命令。
3、grep命令 使用正则表达式搜索文本,并把匹配的行打印出来。
例如:
ps -ef | grep redis-server 以全格式显示redis-server命令的进程信息
1、下载redis
wget http://download.redis.io/releases/redis-6.0.6.tar.gz
2、下载压缩包之后,先赋权限:
chmod -R 777 redis-5.0.4.tar.gz (change mode修改文件读写执行权限)
3、阿里云自带gcc,如果是centos注意安装gcc:
1)gcc和make欲坑参考:Linux安装Redis步骤和make遇到的坑 解决方案.
2)远程连接linux时,需安装telnet服务:
参考:CentOS 7安装telnet服务,使客户端可以远程访问CentOS 7服务器.
4、解压缩、后台启动redis-server(没有后台启动ctrl+c终止【修改bind 0.0.0.0接收所有客户端】)、启动client
tar -zxvf redis-6.0.6.tar.gz
cd redis-6.0.6
src/redis-server redis.conf & (打开redis服务)
src/redis-cli
#密码登录
auth [password]
5、查看网络状态、端口号和PID【+关闭进程】
netstat -tnlp (查看进程情况,没有bind可能redis前是127.0.0.1)
kill -9 [PID] (杀死对应进程)
6、查看文件内容
cat src/redis.conf
1)单机问题:因此采用服务端水平扩展方案。
图片来自于慕课网
2)多个session出现问题:
如果没有引入分布式session时,因为服务部署在不同服务器上,session存储在本地tomcat服务器上,所以通过ngnix反向传输回来,不一定是传给有seesion的那台服务器,从而导致登录失败。
解决方案:
分布式session实现,是将session集中存储在redis中,验证直接从redis中取。
分布式session基于token实现,生成唯一性的uuid作为key,序列化用户信息对象存储在redis中。
这里提到多级缓存存储商品详情页
1)先取本地热点缓存(效果非常好,减少网络开销);
2)再取redis缓存;
3)最后到数据库中查。
1)本地热点缓存(jvm缓存)
本地缓存(就是java虚拟机JVM的缓存)除了运行classloader,还可以管理对象信息。
i、 只存热点数据
ii、 脏读非常不敏感(清除jvm信息很难,内存可控需要把生存时间变得redis生存时间短)
iii、 内存可控
springboot中使用Guava(类似hashmap但是线程安全)
Guava cache
1、可控制的大小和超时时间
2、可配置的lru策略
3、线程安全
2)Redis缓存使用,优化点:在springmvc在controller层引入,将从service层获取的数据缓存起来,下次请求来时直接在controller层判断有没有商品详情页的数据,而不去走下游service层调用,减少对数据库交互。
因存入redis对象需要序列化,顺便复习java序列化:
序列化就是将对象、数值转换成字节序列(数组),写入到文件中去。可以在另外完全不同的平台上反序列化,对于JVM可以反序列化对象,它必须是能够找到字节码的类,就是在该平台上也要定义该类,不然抛出ClassNotFoundException异常。
(序列化对象必须实现Serializable接口,transient修饰字段不会序列化)
商品详情页缓存用户账户:不必每次都去数据库中查询。
缓存的是ItemModel对象(包括聚合PromoModel模型)。
cdn流程:
1、 Dns服务器收到用户访问的cdn域名的cname地址。会转发给当ECS服务器进行解析,然后给用户返回一个就近的cnd节点。
2、 然后cdn节点判断自己有没有需要的静态资源,没有就回源到远端服务器(nginx服务器)抓取数据。
用户不会直接访问远端服务器,会先发送到能解析cdn域名的服务器上,转发用户就近的cdn节点。
Cdn将远方的资源抓取到就近cdn节点,这样用户访问大型静态资源就可以直接从就近cdn内容网络上获取。
页面和资源静态化需要阿里ECS服务器
交易性能瓶颈出现在哪里?Mysql进程繁忙
(1) OrderServiceImpl中createOrder,先校验下单商品(ItemServiceImpl中的getItemById【会查询商品基本信息+商品库存信息+活动商品信息】)
然后校验用户合法(UserServiceImpl中的getUserById),最后校验秒杀活动是否存在(itemModel内存操作)------对数据库发送4条sql。
(2) 减库存操作是热点操作,where item_id=1会加行锁。------减库存行锁等待1条sql
(3) 订单数据入库,插入数据库订单,更新销量 ------对数据库发送2条sql
解决方案:
1、controller层的createOrder接口,从redis缓存中get用户信息、秒杀令牌信息。(秒杀令牌会在进入秒杀页面生成,用户信息在login时,以key为token,value为userModel形式存储在redis)。
2、活动商品判断剥离createOrder接口,放到生成秒杀令牌接口。
3、下单产生的商品信息、库存信息都是操作redis,不直接与mysql交互。交给rockmq消息队列,异步对数据库进行更新,保证最终一致性。
主要是将交易用到的数据库信息缓存到redis中。
1、 用户风控策略优化:策略缓存模型化(用户账号是否存在、异地登录、危险操作等)
2、 活动校验策略优化:item基础信息可以从缓存中拉取,引入活动发布流程,活动紧急下线(活动提前上线)redis清除能力。
优化方法:
1、UserService增加getUserByIdInCache(Integer id)接口方法,直接从redis中取。
2、ItemService增加getItemByIdInCache(Integer itemId)
3、更新库存时,数据库行锁优化,itemId先加唯一索引,(1)扣减库存缓存化(性能消耗很小);(2)异步同步数据库(缓存不可靠需同步到数据库);(3)库存数据库保持一致性。
方案:(1)活动发布时,同步库存进缓存;(2)下单交易时,减缓存库存。(3)异步消息扣减数据库。异步消息队列rocketmq。(阿里基于kafka的优化消息队列)
对数据库进行更新时,不需要立即处理消息。不需要数据库立刻完成更新,所以可以将更新操作变成异步消息,进入消息对列中等待。
异步消息队列rocketmq
1、 高性能,高并发,分布式消息中间件
2、 典型应用场景:分布式事务,异步解耦
Broker消息服务器,用来管理topic、维护消息队列。
Producer业务发起方,负责生产消息传输给broker。
Consumer消息处理方,从broker获取消息进行业务处理。
Topic消息集中地,代表不同类型的消息。
主从brokerA和brokerB数据可以同步也可以不同步。Master brokerA出现问题,就会由slave的brokerB代替。brokerA、B如果同步(保持一致性)性能就会降低,所以brokerB的消息可以异步更新为brokerA。
分布式事务:
ACID刚性事务(强一致性)
本地事务具有强一致性。使用transactional标签维护。
A:Atomicity原子性
C:consistency一致性
I:isolation隔离性
D:durability持久性
CAP理论:
C:Consistency一致性
A:availability可用性
P:Tolerance of network Partition分区容忍性
BASE柔性事务(最终一致性)
BA可用性
S柔性
E最终一致
因此牺牲强一致性来实现高可用。(不追求瞬时状态的一致性,追求最终的一致性)
例如:
Redis中数据状态正确,数据库状态不一定瞬时一致性。
异步同步数据库的流程大致如下:
1、>wget https://mirror.bit.edu.cn/apache/rocketmq/4.7.1/rocketmq-all-4.7.1-bin-release.zip
2、>unzip rocketmq-all-4.7.1-source-release.zip
3、>cd rocketmq-all-4.7.1/
启动nameserver
> nohup sh bin/mqnamesrv &(启动)
> tail -f ~/logs/rocketmqlogs/namesrv.log (查看启动成功)
The Name Server boot success...
启动broker
> nohup sh bin/mqbroker -n localhost:9876 &
> tail -f ~/logs/rocketmqlogs/broker.log
The broker[%s, 172.30.30.233:10911] boot success...
手动创建topic:
./mqadmin updateTopic -n loaclhost:9876 -t stock -c DefaultCluster
查看topic:
sh mqadmin topicList -n localhost:9876
Spring整合rocketmq
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
Remote address问题:【最后重启一下就能够通信rocketmq了】
参考:https://blog.csdn.net/lwycc2333/article/details/106629237
1)首先查看防火墙的状态
systemctl status firewalld
2)如果防火墙开着,那么将其关闭
systemctl stop firewalld
防火墙通过端口9876 ,开放80-12000
需要重启防火墙配置生效。
本质是分布式事务问题,(落单减库存这个操作是分布式操作不能保证最终一致性—1、发送异步消息;2、扣减操作失败;3、下单失败无法正确回补库存)在我们执行完这个分布式事务后,没办法保证后续的操作(订单入库是否执行失败),如果执行失败,分布式执行操作没法办回滚(会发生少卖现象,库存明明减少了但是却没有对应的订单。)
事务型消息:
1、 mysql事务成功提交,消息也一定会成功发送。
2、 mysql事务回滚了,消息必定不发送。
3、 mysql事务未知,消息pending等待中的状态。
sendMessageInTransaction做两件事,1、往broker投递一个prepare消息;2、本地执行localtransaction(创建订单)
Prepare状态下消息不会被消费者看到。等待execute方法执行结束。
Execute执行createOrder方法,返回state,如果是commit就是执行成功,如果是回滚消息或其他就是执行失败。
重点:
1、做了秒杀令牌promoId_userId_itemId_token用来解耦(活动时间校验逻辑,秒杀下单逻辑)。
2、限制秒杀令牌个数,限制秒杀下单的门槛。
3、简单使用线程池+future方式,使用阻塞队列+异步执行同步数据库操作方式来缓解秒杀的高峰。
具体改进过程:
现有缺陷:
1、秒杀下单接口会被脚本不断刷单。
2、秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高。(活动时间校验逻辑,秒杀下单逻辑耦合)
3、秒杀验证逻辑复杂,对交易系统产生额外负担
解决方法:秒杀令牌原理
1、 秒杀接口需要依靠令牌才能进入。(下单createorder之前需先获得令牌,不能直接通过url请求秒杀接口来下单。下单逻辑和校验逻辑拆分,主要是解耦)
2、 秒杀令牌由秒杀活动模块负责生成。
3、 秒杀活动模块对秒杀令牌生命周期全权处理。
4、 秒杀下单前需要先获得秒杀令牌。
发现Bug:在对活动商品进行getItem的过程中,发现页面只能加载第一次,下单也能成功。但是60s后页面就加载不出来了。
原因:因为本地缓存清除时间是60s,debug获取itemModel后发现,在redis内存储的itemModel对象,多出很多字段无法解析itemModel对象,导致getItem页面加载不出来。
方法一,将对象序列化按照java自带序列化方法。但是redis中的内容看不懂。
方法二,自定义序列化方法,datetime->String,String->datetime。并且在字段头加上类型名。
这个Token是UUID生成的。
缺陷:秒杀令牌无限生成,影响系统性能
解决:秒杀大闸原理。
1、根据秒杀商品初始库存颁发对应数量令牌,控制大闸流量。
2、用户风控策略前置到秒杀令牌发放中。
3、库存售罄判断前置到秒杀令牌发放中。
又有缺陷:1、浪涌无法应对。2、多库存,多商品等令牌限制能力弱。
队列泄洪原理:
1、 排队有时候比并发更高效(例如redis单线程模型,innodb mutex key互斥竞争)
有锁时需要cpu切换,直接使用队列排队即可。
2、 依靠排队去限制并发流量
3、 依靠排队和下游拥塞窗口程度调整队列释放流量大小(一次执行复数个操作)
本地OR分布式
本地:将队列维护在本地内存中(线程池20)
分布式:将队列设置到外部redis内(分布式队列,性能消耗)
需要修改spring内嵌Tomcat参数:
在application.properties中添加
Server.tomcat.accept-count=1000 //等待队列长度1000比较合理
Server.tomcat.max-connections=10000 //单机最大连接数10000合理
Server.tomcat.max-threads=800 //单机4核8g 800个线程最好。
Server.tomcat.min-spare-threads=100 //最小工作线程数
链接: link.