技术点总结
beanfactory就是一个bean的工厂,Ioc通过它来管理bean,applicationContext继承于beanfactory,beanfactory是给内部人士用的,applicationContext是一个子接口,功能更强,给开发者使用的。
后端部分
工具箱中新建方法:md5加密方法
还有一个日期的格式化方法
在controller中使用统一异常处理
springmvc提供一个机制,类似于aop,只针对springmvc这一层。当任何一个controller报异常时,就会调用此方法处理。
很多地方都需要去验证用户是否登录,在这样情况下,很容易就想到aop实现,但是在这种情况下,应该使用拦截器实现。Aop是面向方法名,面向方法去过滤的。拦截器是面向url去过滤的。Filter是更高层的过滤,一般不用filter。
主要关键是说清楚拦截器,以及拦截器的三个方法的执行时机。为什么要使用prehandle。
cookie是会员卡,用户可以带走,不安全,可以篡改。session是保险柜,存放在服务器上,用户只保存一个保险柜编号,比较安全。敏感信息使用session。cookie携带的信息有限,2k。session不限制。
但是上面的情况只适用于单个服务器的情况,不适用于分布式。
分布式:使用redis存储session。
多个服务器就可以共享session。
1、下单操作,jd发现他没登录,重定向让用户去sso登录,并且携带jd的路径。
2、去sso进行登录。给用户返回一个登录页面
3、用户提交登录信息,登录成功,sso给用户一个全局tocken,并且重定向回到jd下单页面。
4、用户携带tocken去访问jd,jd要验证tocken,去向sso验证tocken,得到结果有效。jd给用户发一个自己的tocken_jd。
5、用户去访问nc,重定向让用户去登录。
6、用户访问sso进行验证,发现用户已登录,重定向让它去访问nc(携带tocken返回)
7、用户携带tocken去访问nc,nc向sso验证,得到有效信息。返回一个局部的tocken_nc。全局tocken设置30分钟有效期(无操作30分钟后),有效期到之后销毁的同时通知其他子系统将tocken销毁。
商品表item
商品库存表item_stock
商品活动表promotion
除了自动生成的方法外,手动补充一些方法。
前端逻辑大致了解就行。
使用explain查看sql语句的执行效果,能看到是否使用了索引。type=ALL,表示全局扫描。rows表示预计扫描30条数据。
普通索引、唯一索引、全文索引、空间索引
按照存储方式:B-tree、Hash
按照列数分类:单列索引、组合索引
按照数据分布:聚簇索引、二级索引(辅助索引,索引的索引)
按照回表情况:覆盖索引
1、表结构设计
item:订单将来很庞大,肯定要把历时订单的数据拆分出去,所以设计表的时候,以年月日开头,后面加一个流水号,便于后面的表拆分
serial_number:专门为了记录订单生成的流水号,每次查流水号的时候,会给它for update,加一个行锁,避免两个用户的订单号重复。【查流水号是因为要创建新订单,更新流水号,所以要加锁。】
2、秒杀问了 redis 和数据库的同步怎么解决的
3、消息丢失 重试什么的
4、秒杀这一块你当时是怎么设计的,怎么想的。
5、redis缓存和库存数据的一致性问题怎么保证的。
6、消息没有消费怎么办。
7、消息丢失怎么办。 发到mq之前丢了,发送到mq没人消费。
8、死信队列怎么处理。
9、秒杀什么保证了竞争的并发安全。
10、什么命令是原子操作的。
11、你有对扣减到负数库存做校验吗。
12、异常如何管理的。
13、数据库有加锁的操作吗
14、怎么解决超卖问题?
下单时要扣库存。所以要锁库存,锁库存表即可,不用锁商品表。
15、什么情况下会触发行锁。
慢查询分析
1、开启慢查询日志:set global slow_query_log = ''ON; 2、设置超时时间:set global
long_query_time = 1;
p5-p6简书排版
启用了慢查询日志,看到,某一个sql执行时间大于0.1s,我认为比较慢,通过explain命令去查看后发现本次查询没有通过索引,但是我创建了索引,为什么没有走呢,可能是索引的条件不符合最左前缀原则,所以针对这个问题去解决,然后性能就提高了。编一个这样的故事。有理有据。
下单的时候要判断商品是否参与了活动,如果参与了活动就要修改商品价格。这两个操作要保证事务性。
少卖问题
下单时锁库存,不会产生超卖问题,但是有可能产生少卖问题,解决方法是超时自动释放订单,回补库存。
超卖问题
付款时锁库存,会产生超卖问题,即下单的人无法付款,库存不够。体验不好。
所以我们项目中采用的方式是下单时锁库存,比较好一点。
数据库表
查看订单表order_orderinfo
order_info
订单id自增,为了生成订单id设计的表。那当前id自增到了多少,这个数字需要另外去保存一下,不能保存内存,要持久性存储。serial_number表就是专门用来记录索引的最大序号。这个表同样可以存放其他表的最大值。
order_serial
SerialNumberMapper
此时加入x锁的目的是为了不让读取历史版本的更新,而是强制使其读取最新的数据,而最新的数据有人正在改,那就互斥,等待其改完之后再读。p6:15:14
前端页面item.js
事务的隔离级别
锁的粒度
表锁:不是锁整个表,实际可能锁锁一页就可以。
行锁:锁的也不是一行,是锁这一行前后的数据。
共享锁
排它锁
意向共享锁
意向排它锁
锁的机制
查询的时候同样可以加锁,并不是只有修改的时候可以加,如果想在查询的时候显示的加锁,就可以使用上面的两条语句;1是排它锁,2是共享锁。
select … for update
死锁
注意,一个事务读取到其他事务未提交的事务。而一个事务读取到undo log中的历时数据,不叫脏读。
Read Commited :record Lock解决了脏读问题,MVCC提高了并发性。
binlog和redo log 的区别:
bin log记录的是sql,redo log存储的是二进制数据。
bin log是事务提交时记录,redo log是每次操作都会记录
redo log是InnoDB独有的日志。
框架处理事务的方式
使用@Transactional注解,自动处理异常,自动begin、自动commit,实际底层是aop实现的。
事务的传播方式
在Spring中对于事务的传播行为定义了七种类型分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
主要压测商品详情页和下单功能
遇到问题:ping 114.116.228.176 请求超时 在安全组中配置,一键放通,ICMP:全部
步骤:
1、修改项目的配置
本地是dev环境,服务器是linux,是test环境,需要修改对应的配置文件,再进行上传
1、修改application-test.properties
2、项目打包
mvn clean package -Dmaven.test.skip=true
3、将项目jar包上传到服务器。在到target目录
scp seckill-0.0.1-SNAPSHOT.jar [email protected]:/root
4、在服务器安装jdk
yum list java* yun install -y java-1.8.0-openjdk.aarch64
5、测试java安装是否成
java version
6、将脚本上传到服务器中。
scp startup.sh [email protected]:/root
7、开启权限
chmod -R 777 startup.sh
8、启动tomcat服务器
sh startup.sh
9、查看启动信息
tail nohup.out
10、安装Nigix
yum list nginx* yum install -y nginx.aarch64
11、将前端页面传到服务器上的指定位置,在指定文件夹下打开cmd窗口
scp seckill-site.zip [email protected]:/usr/share/nginx/html
12、解压缩文件
unzip seckill-site.zip
13、修改/usr/share/nginx/html/seckill-site/js下的common.js文件
vim common.js
修改为114.116.228.176:90端口
14、配置nginx,使其80端口代理前端服务器,90端口代理后端端口。
进入目录:/etc/nginx 配置:vim nginx.conf
15、重启nginx服务器
nginx -s reload
配置nginx时遇到若干问题
16、访问114.116.228.176
安装jmeter,配置jmeter.properties中的编码方式为UTF-8,配置jmeter到环境变量中。配置jmeter,进行商品详情页压力测试。
商品详情页压力测试:
结果如下:结果不好
下单操作压力测试:1:21
imeter进行压力测试,通过post请求访问下单操作,携带参数itemId,amount,promotionId,还要携带用户身份,携带cookie模拟实现。
1000个线程模拟1000次下单测试,带来的压力和1000个用户各下单一次是差不多的。
Nginx一般做热备
tomcat服务器做分布式
mysql一般不做分布式(很麻烦),但是做读写分离。
将来可以优化的地方:负载均衡、分布式服务器、读写分离、本地缓存、redis二级缓存(Redis集群)、消息队列
消息队列
数据库内部的分布式事务【解决bin log和redo log 的同步问题】
怎么解决呢?
yum list redis*
yum install -y redis.aarch64
vim /etc/redis.conf
打开行号 :set nu
69行:注释,就可以通过内网和外网去访问
136行:daemonize yes 后台运行
507 :解除注释,设置密码:333
保存退出
redis-server /etc/redis.conf
我们去自定义一个RedisTemplate(在Redis源码中,当我们自定义一个RedisTemplate,就不会去调用SpringBoot默认的RedisTemplate了),主要 是提供序列化的方式
systemctl stop firewalld.service
ctrl+shift+f
request是请求对象,请求中的所有数据都会在request中。请求行,消息头,实体等等都可以通过request获得。
是不是在每次请求的时候通过cookie携带了tocken?如果用session的话,session会给客户端返回一个cookie,cookie里面保存了sessionID,浏览器会将cookie自动地保存在浏览器的内存或者硬盘中。【session自动处理】
而tocken是一个类似的东西,它存到哪了呢?【在前端js代码中手动处理】
这个tocken保存在本地的浏览器的sessionStorage中,比较安全,其他人得不到。
因为Redis是基于内存实现的,空间有限,很宝贵。需要合理的编码设计。
IO多路复用2:00
分布式:服务器扛不住。分布式是一定要用的。
微服务:不是必需品。项目规模太大了,业务功能要拆解。一个简单的微服务系统有几十个子系统,拆解也会带来复杂的问题,开发难度大,不到迫不得已不会用微服务。微服务一定是基于分布式。微服务主要就是两种体系,dubbo和Spring cloud
Redis的持久化方式
Redis支持RDB和AOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数 据恢复。理解掌握持久化机制对于Redis运维非常重要
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发
AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用 是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式
不需要将下面的所有命令都记录下来,因为最终把name删除了,上面的命令没有用。所以冗余了。所以aof文件会进行重写,删除冗余的命令。重新生成一个压缩版的aof文件。
解决方法:
1、缓存空对象
2、布隆过滤器
解决方法:
1、热点数据永不过期
2、加互斥锁
解决方法
1、避免同时过期
2、降级和熔断
3、集群模式,实现高可用。
缓存的思想,下单操作可以实时执行,但是扣减库存和销量递增可以通过缓存,异步处理。提高性能。
# 开放端口
9876, 10909, 10911
# 安装
wget https://mirror-hk.koddos.net/apache/rocketmq/4.8.0/rocketmq-all-4.8.0-bin-release.zip
unzip rocketmq-all-4.8.0-bin-release.zip
chmod -R 777 rocketmq-all-4.8.0-bin-release
# 配置
cd /root/rocketmq-all-4.8.0-bin-release
# ./bin/runserver.sh (82)
-server Xms256m Xmx256m Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m
# ./bin/runbroker.sh (67)
-server Xms256m Xmx256m Xmn128m
# ./conf/broker.conf (追加)
brokerIP1 = 139.9.119.64
autoCreateTopicEnable = true
# 启动
# namesrv
nohup sh ./bin/mqnamesrv -n localhost:9876 &
tail -f /root/logs/rocketmqlogs/namesrv.log
# broker
nohup sh ./bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true -c ./conf/broker.conf &
tail -f /root/logs/rocketmqlogs/broker.log
# 测试
export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
# 关闭
sh ./bin/mqshutdown broker
sh ./bin/mqshutdown namesrv
一个商品比如说库存有100,所有人都去秒杀这个商品,并发改这个数据,库存和商品是一一对应的,一个商品对应一个库存。所以我们100万人去秒杀同一个商品,我们是改同一份数据,这个并发量很大。但是创建订单不一样,100万人创建订单是插入新数据,互相之间没有影响,而且最终只有100人能够成功创建订单,而修改库存不一样,如果我们去加锁的话,影响就太大了。所以说要把扣减库存这个功能拆出来,延迟处理,只要保证最终扣减库存即可。但是有一个新问题,如果我们始终不去扣减库存的话,那么库存始终为100,那所有人都能秒杀到。这个业务就出错了。所以前面加入了一个预减库存的功能。在缓存中去扣减库存(这样就能提高性能),当缓存中的库存扣光了,那就不能继续创建订单了。最终在mysql中的库存可以延时扣减,延时几秒都能接受。
如果我们在二阶段回查check的时候,要检查“创建订单”这个事务有没有成功,如果没有流水,直接去查订单,此时有两种情况,假设订单创建成功,那没有问题,ok,提交commit。但是如果此时由于数据库阻塞,导致30s后回查的时候订单还没创建出来,可能会在40s的时候创建出来。延迟了不见得一定是失败了。所以回查的时候检查订单不靠谱。业内的解决方案一般是引入流水。
先生成流水,再发送消息。如果流水都没有生成的话,消息根本不会发出。流水生成之后,发送消息,发送消息之后,就执行本地事务,
本地事务创建订单之后,就开始更新流水。那这个流水的状态就是ok的;即使第1步发送消息失败,第4步回滚,回查check的时候检查流水,如果流水状态是ok的,那就说明订单创建成功,那就第7步commit。
扣减库存是100万人同时修改1个数据,但是库存流水不是,每个人创建订单的时候都是去新建一个库存流水,顺序创建,速度很快,不影响。一般来说对硬盘的顺序读写速度可以和对内存的随机写入速度持平。
就是提前把Mysql里的库存挪到redis中,这叫缓存预热。目的就是记录库存的变化,当库存扣减到0后,就不能继续下单了。
更新销量可以异步处理,可以延迟处理。销量晚一点更新,对于购买不会构成影响,但是库存数量影响下单。所以更新销量只需要简单的做一个异步处理就可以,但是扣减库存要保证事务性。
预减库存要在缓存中减库存,前提是缓存中有数据,一般网站的方案是在秒杀活动开始之前就把库存中的数据添加到缓存中【可以用定时器实现】。但是我们没有做那么复杂,就是在秒杀之前,使用自定义方法模拟,将数据库库存信息添加到缓存中。
使用事务性消息保证预减库存和最终扣减库存的一致性。
在交易环节去限制流量就晚了,所以之前单独做一步验证的逻辑,去限制流量,比如说有100个库存,现在通过验证环节削减流量,只放1000人进来交易。
验证通过后不是直接去交易,而是给通过验证的用户发一个令牌(凭证),交易环节用户携带令牌,有令牌就能交易,没有令牌就出去。
此时出现一个新问题,如果不去限制令牌的数量,100个库存商品,来了100万人,没有必要去给这100万人都发令牌,只要发1000个令牌就足够了。这1000人去入围去抢商品就可以了。所以要去限制令牌数量。
大闸就是一个参数,限制了令牌的数量。加入说这个商品的流量是1000,那就把大闸设置为1000,每次发一个令牌,大闸就减1,通过这个数限制令牌的数量。用户做验证时,先看一下大闸,如果还有余量,如果够了就去发令牌,不够就不给发令牌。
此时有一个问题,如果拿到令牌直接就去交易,假设商品库存很多,有30张电影票,1000万人来抢,考虑到有人下单不付款等等情况,30万张电影票需要100万人来抢,就发100万个令牌,这个访问量也太大了,大闸对这种业务场景不太有效。库存很多,的确需要发百万级别的令牌。此时就需要加限流器去限流了。不加限制的话,大量请求访问同一个服务器(Nginx负载不均衡的情况下),就很可能导致服务器挂掉。需要用限流器去限制单机tps,让单机1s钟最多处理1万个请求。不管有多少令牌。
由这个线程池去执行交易的代码。
还有一个问题,这是在请求发送时候做的限制,那能不能在发送请求之前做限制呢?可以在申请令牌之前使用验证码平滑流量。
验证码可以平滑流量,在用户点击下单时,不是马上去抢令牌,如果下单的一瞬间,1000万人都去抢令牌,那nigix的压力很大,所以点击下单的时候,输入验证码,强制让用户慢一点,将1s内的操作放慢到5s内。
验证码机制使用easy captcha,,大闸使用redis缓存,大闸数量设置为库存的5倍。限流器使用guava自带的ReteLimiter。
压测数据可以编一个,不要太夸张,5500。QPS被拒绝了很多,相对较快。TPS每次走到数据库环节,相对较慢。
QPS:Queries Per Second意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
TPS是 TransactionsPerSecond的缩写,也就是事务数/秒。它是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器 做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,最终利用这些信息来估计得分。客户机使 用加权协函数平均方法来计算客户机的得分,测试软件就是利用客户机的这些信息使用加权协函数平均方法来计算服务器端的整体TPS得分。
多线程是解决并发的手段之一,但是不是唯一的手段。
了解并发,首先要对JVM有一个了解。
方法栈中保存方法的栈帧,等方法结束后,栈帧出栈,不需要垃圾回收。并且每一个线程都有自己的栈空间,当我们并发修改方法栈帧中的局部变量值本身的时候,不会存在并发问题。但是修改引用的时候,其实就是修改了堆内存中的值,就会出现并发问题。
1、使用Redis延时队列去解决,隔30s后再去消费一次,把消费失败的再处理一下。
2、使用RocketMq解决,类似redis,但是自带定时器,可以自动检查时间到没到。