秒杀系统优化:
压力测试:
JMeter压测工具:
官网地址:
http://jmeter.apache.org/
下载路径:
http://jmeter.apache.org/download_jmeter.cgi/
用户手册:
http://jmeter.apache.org/usermanual/index.html
添加线程:
添加HTTP请求:
添加cookie:
吞吐量
QPS:每秒查询数;
TPS:每秒事务数;(事务:表示一个请求中包含多个IO操作)
TPS一个请求包含多个操作;
QPS一个请求只有一个操作,或者只有查询操作;
测试出现的问题:
库存变成了-100;超卖的问题,不合理,库存不够就不能减库存了。解决:加同步锁,
有问题,不好锁,JVM的内存(vo)和数据库操作,锁的粒度太大了性能太低了。在减库存之前先去查询库存够不够,再进行加锁,可以减小锁的粒度,但是性能还是很低。
如果部署到多个服务器上面,就要为每一台机器都加锁。存在并发的问题。只能加分布式锁。
线程共享数据的安全性问题
优化过程,后面往前面,从数据库往浏览器的方向逐步优化。
分布式锁:
优化1:乐观锁解决库存超卖问题:
乐观锁
:核心思想
版本号
把更新的值当做版本号来使用。
DML 语句操作之后,会返回当前执行的语句受影响的行数。返回值 == 0 表示修改失败。
SQL减库操作多加一个条件:只有当库存 > 0 的时候才会修改成功,否者都返回 0。是为了避免当有多个请求由于多线程的问题而进入数据库进行减库操作的时候,库存没有以后,其他的请求都操作失败。
优化2:用Redis预减,对访问数据库的请求做拦截·
问题:目前发起秒杀的请求都是直接访问到数据库,虽然只有少部分的请求能够成功。这样高并发环境下数据库压力会很大。而且这样做行程长,连接数据库等等开销大,耗时长,性能低。
考虑在进入数据前进行拦截:
方案一:在程序中设计一个Map做缓存
(
分布式问题
):key: 秒杀id, value: 秒杀库存数量 对访问的请求做预减操作。
但是这样直接在JVM中内存中操作,存在一个分布式(JVM做分布式部署的时候,每个JVM的存储空间互不干扰,各做各的预减不能保证原子性)的问题;
方案二:在Redis中做预减
JVM内存中存在分布式问题,以前的解决方案:
将数据迁移到Redis中
,在 Redis 中加一个标记:key : 秒杀id, value : 秒杀库存数量
调用decr方法对value做递减操作。返回递减完以后的值。当库存数量 <= 0 以后,表示库存不足了,将后面的请求全部返回错误信息提示,这样MySQL数据库的压力就减小了。
热点数据:可以进行
数据预热
;开始时间:创建秒杀活动之后,直接将秒杀信息添加到redis中提前准备好。
数据类型:选择Hash;hash里面可以装多个键值对;
模拟数据预热:
预减判断:注意条件是 < 0 而不是 <= 0
如此,库存以外的大量请求都在进入Redis对值进行预减以后,因为条件不满足而被拦截住了,只有满足条件的请求才会进入到MySQL数据库中对库存进行减操作。
数据丢失:只会有少量因为丢失数据的漏网请求进入数据库减库存操作,不满足乐观锁的请求最终还是不成功的。这里仅仅是初次过滤,真正到的操作还是再MySQL中。减少失败请求的压力而已。
优化:当预减数据小于 0 以后,剩下的请求都没有必要在进入Redis中进行预减了。
优化3:本地售完标记:预减的库存小于0以后,剩下的请求都没有必要在做减库存操作了
用JVM本地内存的
并发的
ConcurrentHashMap
来标识库存数量是否还可减,如果状态变成false,标识不可减了,不放请求过去,避免了操作Redis数据库,速度更快了。
为什么选择:
ConcurrentHashMap可以防止多线程并发的安全问题
。
本地缓存的
ConcurrentHashMap
对象:
当预减库存 < 0 之后,标记状态不可减:
判断的位置 / 时机:放在最前面。
因:库存都没有了,后面的所有满足条件的判断都没有必要了。
优化4:用Redis的Set类型,做重复下单用户的判断
用户是否已经下过单的优化:在Redis中用 set 判断用户是否下过单。
当用户秒杀成功,将该成功的用户保存进Redis中的 set 中,判断是否下过单,根据set中是否存在该value,来决定是否放行。
什么时候将用户 id 存进去?当用户下单成功,生成订单的时候就表示用户购买成功了。key:前缀:秒杀场的id,value:userId
在订单创建结束后,将用户添加进set中。
判断当前用户是不是重复下单:
此时有一个问题:假设某第一次开抢的用户,用奇快的手速发出了三个请求,成功的绕过了isMember的检查,那么在创建订单得时候,就出现了三个请求同样的userId下单成功了,就出现了重复下单的问题了。
解决方案:
在创建订单之前加个分布式锁,去数据库查询一下该用户是否已经下过单,如果已经下过单就抛异常回滚。就是让刚刚的三个请求线程变成串行的执行。
简单方案:利用MySQL数据库里的唯一索引来约束插入的数据,当SeckillId和userId都相同的时候就只会插入成功一条,第二条就会插入失败。弊端:可能会影响到数据库的所引述维护问题。
多线程分布式的应用:时刻关注
并发安全问题
,都要注意判断是不是原子操作。
数据库的
唯一索引
,设计表的唯一索引约束;作为最后一道防线,解决判断是否重复下单判断没有原子性,在多线程情况下可能一个用户有两个下单请求进入的情况,再做唯一索引时会添加错误。加唯一索引可能会影响到数据库的
索引树维护
的问题。
另一种方案:
分布式锁
。判断/存储加上分布式锁,作为一个原子操作,操作会复杂一些。
优化5:用Redis的Hash类型对秒杀商品数据进行预热
秒杀商品的信息也属于热点信息,可以进行预热优化。
将vo对象存进hash中,在Redis中一个key对应一个hash,这个hash里存着多个键值对。(注意:Redis中大key存储的数据不能很大),用string的话会产生比较多的key。
位置:和库存预热一样,在秒杀活动创建完成之后就将数据存储进Redis中。
那它能替代下面的吗?
不能,如果替代,预减库存的时候获取vo对象,get库存再做减库存的话,此时操作的是JVM内存中了,又有线程安全的问题了。所以各管各的。
查数据是去Redis中查,定义查询的方法:
检查条件:
优化6:用Redis查询秒杀商品列表(没有高查)
查询秒杀商品列表(没有高查):也可以去Redis中查询。
注意:
前端显示的页面信息应该显示的是查Redis中的数据,而不是去MySQL中查询。因为MySQL中请求有可能还没有操作完成,前台就显示出来了,这种情况是不准确的。所以最新的页面消息应该从Redis中查询到。
创建订单的时候查询vo对象的方法也改为在Redis中查询:
流程中还存在的问题:
抛出异常的类型还可以优化一下,将CodeMsg封装进result中,返回给前台提示,而不是通过异常的方式来提示。
极致提升:框架的提升,去框架,避免不必要的流程。
JVM优化;
连接池优化:应用里的线程池、MySQL、Redis;
集群,流量监控(缩短物理距离);
静态页面等静态信息缓存前置化。
异常: