秒杀问题梳理

所有优化手段总结

选课: 差额和等额。差额只有一门课,全校都会选的公共课。

等额:专业课,每个人只要想选就能选到。等额:一门课有不同场次,每个人选偏好。(课程id,preference,学生id)偏好从1递增。一个科目,三个可选时段的课abc,三个固定队列。map维护选了这个课程的学生,按优先级依次push,push完标记map里的。if(count 学生id<课剩余总数),全push进去。else,生成不连续的随机数。while size<10, set.add。

差额:zookeeper: interprocessmutex(acquire,release)定时任务,学分不够邮件提醒。消息队列异步进入tomcat。预约选课,增加缓存命中。

1.缓存预减:redis
2.内存售完标记:内存
3.流量削峰:消息队列异步处理订单,架构而言介于数据库之上
4.接口限流:nginx,计数器,滑动时间窗口,令牌桶(漏斗算法),hystrix。
5.防刷验证码:随机数。
6.秒杀下单接口地址隐藏(盐值)
7.页面静态化:cdn
8.redis集群的高可用

一个简单的流程:

1.系统初始化,把商品库存数量stock加载到Redis上面来。
2.请求通过nginx分发到不同的tomcat
3.后端收到秒杀请求,判断用户登录与否。
4.登录了的话,就去找内存标记,这个内存标记是分布式的,所以也是有一个同步的过程。我用的zookeeper的watcher机制实现的。
5.如果内存标记为false或者空,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败。
6.在缓存判断这个秒杀订单形成没有,即是否已经秒杀到了,避免重复秒杀。(这方面我是在sql生成订单后写给redis,这也是个事务)
7.库存充足,且无重复秒杀,将秒杀请求封装后消息入队,同时给前端返回一个code (0),即代表返回排队中。(返回的并不是失败或者成功,此时还不能判断)
8.前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(考虑200ms轮询一次)。(待优化)
9.后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这名字的通道,如果有消息过来,获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单,写入秒杀订单)。
10.此时,前端根据商品id轮询请求接口MiaoshaResult,查看是否生成了商品订单,如果请求返回-1代表秒杀失败,返回0代表排队中,返回>0代表商品id说明秒杀成功。

在这里插入图片描述

缓存雪崩、缓存穿透、缓存击穿

缓存雪崩:大量缓存集中失效。一般是服务器宕机。
解决方案:
1.redis集群。
2.失效后,给请求限流。即通过锁或者队列空值读mysql的线程数。
3.也有可能是设计问题,注意在数据预热的时候均匀设计过期时间。

缓存穿透:redis和mysql中都没有的东西。恶意防刷。
解决方案:
1.对查询为空的也设置缓存。但是缓存的时间短一点;或者insert的时候删掉。
2.bitmap。类似布隆过滤器。不存在的key筛选掉。

缓存击穿:热点失效
解决方案:
1.互斥锁:
在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
2.不过期

超卖

https://blog.csdn.net/fly_miqiqi/article/details/104346463

  1. 数据库的行锁。 stock>0 高并发下 效率低。由于这种写请求很多,会造成大量的请求超时,连锁反应就是应用系统连接数被耗光,直至系统异常crash。即使重启系统,由于请求量大,系统也会立马挂掉。因此redis预减库存。互补关系。
    https://blog.csdn.net/qq_30285985/article/details/114268723
    for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。当一个事务的操作未完成时候,其他事务可以读取但是不能写入或更新。
    利用innodb的行锁,mysql会把线程排队。
    在这里插入图片描述

2.乐观锁
select version from goods WHERE id= 1001
update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);
其他99个在更新的时候,会发觉version并不等于上次select的version,就说明version被其他线程修改过了。那么我就放弃这次update

3.分布式锁
利用redis的单线程预减库存。比如商品有100件。那么我在redis存储一个k,v。例如 <gs1001, 100>
每一个用户线程进来,key值就减1,等减到0的时候,全部拒绝剩下的请求。
那么也就是只有100个线程会进入到后续操作。所以一定不会出现超卖的现象

https://www.cnblogs.com/dawuge/p/10480469.html

考虑到给数据库加锁效率低,所以采用redis+lua来进行实现 。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
Nginx 支持 Lua ,Redis支持Lua,面对高并发可以选择Nginx+Lua+redis,替代掉Tomcat+mysql
电商 最热访问页面,很多都是使用Nginx+Lua+redis

缓存一致性

一致性三种方式:
1.缓存设置过期,自动到期。
2.定时全量更新。
3.先更新再删除redis,有一点问题,见下。
各自问题都很明显。

要求数据更新的同时缓存数据也能够实时更新,怎么办?
先说结论:
解决方案:
在数据更新的同时立即去更新缓存,首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新,需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。??

两种策略:
1)先删缓存,再更新数据库
同时有一个请求A进行更新操作,另一个请求B进行查询操作,c读到脏数据。采用延时双删策略
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
吞吐量降低怎么办?第二次异步,开辟新线程
第二次删除失败?2里面有解答。
2)先更新数据库再删缓存
这是根据《Cache-Aside pattern》得出的结论。
想象一个场景:a查旧—b更数据库—b写缓存—a写缓存。
但是查一定比写快。如果真的不放心,设置缓存失效期,并采用延时双删。
1)2)都有的问题是删除失败怎么办?

第一种方法:
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功 然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
第二种方法:
在这里插入图片描述
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。

主从数据库一致性:
https://www.cnblogs.com/yefeng654321/articles/11257697.html 半同步复制
主从数据库的一致性,通常有两种解决方案:

  1. 中间件
    输入图片说明
    如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。这个方案的缺点是,数据库中间件的门槛较高(百度,腾讯,阿里,360等一些公司有)。

  2. 强制读主
    在这里插入图片描述
    上面实际用的“双主当主从用”的架构,不存在主从不一致的问题。

第二类不一致,是db与缓存间的不一致:

输入图片说明
常见的缓存架构如上,此时写操作的顺序是:

(1)淘汰cache;

(2)写数据库;

读操作的顺序是:

(1)读cache,如果cache hit则返回;

(2)如果cache miss,则读从库;

(3)读从库后,将数据放回cache;

在一些异常时序情况下,有可能从【从库读到旧数据(同步还没有完成),旧数据入cache后】,数据会长期不一致。解决办法是“缓存双淘汰”,写操作时序升级为:

(1)淘汰cache;

(2)写数据库;

(3)在经验“主从同步延时窗口时间”后,再次发起一个异步淘汰cache的请求;

这样,即使有脏数据如cache,一个小的时间窗口之后,脏数据还是会被淘汰。带来的代价是,多引入一次读miss(成本可以忽略)。

在这里插入图片描述
除此之外,最佳实践之一是:建议为所有cache中的item设置一个超时时间。

层次

https://www.cnblogs.com/hadley/p/9459740.html
前端

用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务。

突然增加的网络及服务器带宽
假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G,这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。

站点层

程序员写个for循环,直接调用你后端的http请求,怎么整?
(1)同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
(2)同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面

阿里的Sentinel、Hystrix等?

后端

分层校验的核心思想是:不同层次尽可能过滤掉无效请求,只在“漏斗” 最末端进行有效处理,从而缩短系统瓶颈的影响路径。
1.对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”。
秒杀商品和普通商品的减库存是有差异的,核心区别在数据量级小、交易时间短,因此能否把秒杀减库存直接放到缓存系统中实现呢,也就是直接在一个带有持久化功能的缓存中进行减库存操作,比如 Redis?
如果减库存逻辑非常单一的话,比如没有复杂的 SKU 库存和总库存这种联动关系的话,个人认为是完全可以的。但如果有比较复杂的减库存逻辑,或者需要使用到事务,那就必须在数据库中完成减库存操作。

库存数据落地到数据库实现其实是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁。但并发越高,等待线程就会越多,TPS 下降,RT 上升,吞吐量会受到严重影响。

解决并发锁的问题,有两种办法:
1)应用层排队。通过缓存加入集群分布式锁,从而控制集群对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用过多的数据库连接
2)数据层排队。应用层排队是有损性能的,数据层排队是最为理想的。业界中,阿里的数据库团队开发了针对InnoDB 层上的补丁程序(patch),可以基于DB层对单行记录做并发排队,从而实现秒杀场景下的定制优化——注意,排队和锁竞争是有区别的,如果熟悉 MySQL 的话,就会知道 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换都是比较消耗性能的。另外阿里的数据库团队还做了很多其他方面的优化,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的补丁程序,通过在 SQL 里加入提示(hint),实现事务不需要等待实时提交,而是在数据执行完最后一条 SQL 后,直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚,减少网络等待的时间(毫秒级)。目前阿里已将包含这些补丁程序的 MySQL 开源:AliSQL,专门应对这种情况,可以了解下。

2.对于读请求,用cache来抗,不管是memcached还是redis,单机抗个每秒几万都是没什么问题的;如此限流,只有非常少的写请求,和非常少的读缓存miss的请求会透到数据层去。

用户请求分发模块:使用Nginx将用户的请求分发到不同的机器上。

用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。
经过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,如果后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败即可

用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。
数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。

如何从0优化秒杀?

注:包含了redis和内存,两级缓存。
单机mysql支持的并发数量级大概是几百。throughout:300
库存放到redis缓存,让先来的预减redis库存。
放到redis:throughout:1000
在这里插入图片描述
减少库存:
在这里插入图片描述
redis-1成功,mysql扣库存失败?缓存不一致了。
在这里插入图片描述
刚才是redis一级缓存,一般系统都是多级缓存?
过滤无效的请求,前100次有效,后9900次没必要redis预减库存。
一旦redis库存减完,在jvm中做一个内存标记,表示已经库存已经没了。redis虽然快,但是也是个远程服务,有网络开销。
在这里插入图片描述
在这里插入图片描述
加了内存标记,但是万一后面sql操作失败了,是不是又应该还原?

在这里插入图片描述
throughout:2000以上

分布式部署:springboot里application.properties改一個端口号server.port即可

问题:redis是没问题的,但是jvm级别的缓存有问题。productsoldoutmap内存标记有问题!没有做同步。可以用mq或者zookeeper同步。

zookeeper配置:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
watcher:
在这里插入图片描述
在这里插入图片描述
一旦节点数据变化,会触发事件回调方法,然后再这个事件里remove掉这个map里对应的productID。相当于zookeeper监视了所有tomcat里jvm的soldoutmap,

怎么保证用户不重复下单?

解决:
1.提前判断是否生成订单。
2.为了安全起见,秒杀订单表中建立一个唯一索引(所引是用户Id与商品goodsId),使得第一个记录可以插入,第二个则出错,然后通过事务回滚,防止一个用户同时发出多个请求的处理,秒杀到多个商品。

数据库加锁如何优化?

Mysql5.5 mysql5.6 mysql5.7 :默认的最大连接数都是 151 ,上限为: 100000
Mysql5.0 版本:默认的最大连接数为 100 ,上限为 16384

这样一个事务:
BEGIN;
INSERT INTO stock_log VALUES
SELECT count FROM stock WHERE id=1 AND count>0 FOR UPDATE;
UPDATE stock SET count = count -1 WHERE id=1 AND count > 0;
COMMIT;
秒杀操作虽然是并行的,但是在数据库层面是串行的。尽管进行了限流,但是这地方效率仍然有问题。随着并发的不断增大,不断发生事务的锁等待与唤醒操作,导致性能的急剧下降。
两种想法:

  1. 全放在redis上做,用lua实现。
  2. 如果通过perf工具来观察的话,应该可以观察到类似如下的内容:
    59.06% mysqld mysqld [.] lock_deadlock_recursive
    16.63% mysqld libc-2.13.so [.] 0x115171 3.09% mysqld mysqld [.] lock_rec_get_prev
    2.96% mysqld mysqld [.] my_strnncollsp_utf8
    可以发现锁的死锁检测占据了大部分的CPU时间,究其原因,就是因为锁等待。
    可以通过innodb_thread_concurrency参数来控制InnoDB存储引擎层的并发量。通过这个参数可以限制进入InnoDB引擎层的事务数量,对比测试的话,性能上的确会有一定的提升。
    将innodb_thread_concurrency设置为16,性能的确会有一定的提升。并发线程数在128的时候,TPS从原有的4300提升为了7200,将近有65%的性能提升。但是在256线程之后,性能依旧堪忧。
    线程池可以在MySQL上层限制住同时运行的MySQL的事务数,这样就解决了由秒杀而导致的资源竞争问题。
    通过前面的测试,已经得知并发16线程时,秒杀可以有最好的性能,那么这时用户将线程池的大小设置为16,这样就能获得用户预期想要的性能。
    将参数thread_pool_oversubscribe设置为1

其实秒杀应用的数据库层优化非常简单,各个层面做好排队即可,如:MySQL的线程池允许有额外的线程运行。该参数默认是3,之前thread_pool_size设置为16,那么总共允许16*(1+3)=64个线程同时运行。

应用层做好对于单个商品抢购的数量限制
MySQL数据库层使用线程池技术来保证大并发量下的性能
调整参数thread_pool_oversubscribe用来进一步提升性能
MySQL企业版提供了线程池插件,但是需要额外的费用。小伙伴们可以使用开源的MySQL版本InnoSQL,其免费提供了线程池,可以保证应用在大并发量下依旧保证应用的稳定性,特别是对于秒杀类的应用。
https://www.cnblogs.com/cchust/p/4510039.html
Mysql-Server同时支持3种连接管理方式,包括No-Threads,One-Thread-Per-Connection和Pool-Threads

别的思路:
库存拆分
将同一个商品的库存记录拆分为多行甚至多个表,降低并发冲突。举一个简单的例子:对业务请求中的userId计算hash取模后确定查询哪个库那张表的哪行记录,然后在做库存更新操作。这样能够在业务层极大的降低并发冲突,不需要数据库做相关优化,是成本较低收效较大的一种方案。但是该方案也有一个缺点:由于同一个商品的库存记录分散到了不同库表中,那这些商品的库存扣减速度不均衡(热点商品在短时间内被秒光,这个问题并不严重),给总库余额计数带来的复杂度。

批处理
修改数据库内核代码,将相互冲突的事务,合并为一个事务或者优化为组提交,达到批处理的效果,AliSQL的做法是在MySQL server层识别这类update语句,将它们解析后合并成为一条SQL再执行。比如10个扣减库存语句,合并为一个扣减库存的语句一次性扣减数量为10,这样相当于提高了数据库10倍的吞吐量。

请求排队
在数据库内核层面引入上述“批处理”的优化后,对热点数据的并发扣减库存业务仍然会面临多个事务并发进入临界区的情况,并发等锁的事务会占据宝贵的连接和线程资源。为避免并发事务因抢占锁而造成的资源浪费,可以在数据库内核层面将冲突事务排到一个队列处理,这样就可以解决并发冲突,降低系统负载。

分布式事务

需不需要分布式事务?mysql集群的话是需要的。加下mysql主从。
暂略
1.两阶段提交(2PC)
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
2.TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
本地消息表(异步确保)
3.本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
4.MQ 事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。

–数据库一致性中的删除问题
canal订阅binlog 暂略

redis集群

如何使用分布式锁?舍弃了分布式锁,正常要用redlock解决,要考虑宕机,死锁的场景。暂略。

秒杀事务的内容

1.mysql扣库存2.mysql加课表3.redis加秒杀信息。

–redis集群需要同步时间
这期间会有什么问题?
redis同步时间是多少?限制一个uid请求间隔。
https://www.cnblogs.com/leyangzi/p/11172470.html

redis链接池配置

https://www.cnblogs.com/liuqingzheng/p/11080680.html

#redis配置项
redis.poolMaxTotal=1000
redis.poolMaxldle=500
redis.poolMaxWait=500

redis集群配置

每台服务器上开启一个redis-server和redis-sentinel服务
sentinel:
cp /usr/src/redis-3.2.1/sentinel.conf /usr/local/redis/
vim /usr/local/redis/sentinel.conf
内容如下

protected-mode no
daemonize yes
port  6800
sentinel monitor mymaster 192.168.68.110 8000 1
sentinel auth-pass mymaster 123456
#5秒内master6800没有响应,就认为SDOWN
sentinel down-after-milliseconds mymaster 5000 
sentinel failover-timeout mymaster 15000
logfile  /var/log/redis/sentinel.log
pidfile  /var/run/sentinel.pid

redis:

修改配置文件
cd /usr/local/redis
vim redis.conf

主节点192.168.68.110上的配置
port  8000         
daemonize  yes
bind  192.168.68.110
requirepass 123456
pidfile   /var/run/redis-8000.pid
logfile   /var/log/redis/redis-8000.log

从节点192.168.68.111,192.168.68.112上的配置
port  8000         
daemonize  yes
bind  192.168.68.111  #192.168.68.112
requirepass 123456
masterauth 123456
pidfile   /var/run/redis-8000.pid
logfile   /var/log/redis/redis-8000.log
slaveof  192.168.68.110 8000

这里的哨兵有两个作用
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值