1.秒杀系统的设计
秒杀一般出现在商城的促销活动 中,指定了一定数量(比如:10个)的商品(比如:手机),以极低的价格(比如:0.1元),让大量用户参与活动,但只有极少数用户能够购买成功。这类活动商家绝大部分是不赚钱的,说白了是找个噱头宣传自己。
像这种瞬时高并发的场景,传统的系统很难应对,我们需要设计一套全新的系统。可以从以下几个方面入手:
页面静态化
CDN(content delivery Network)加速
缓存
mq异步处理
限流
分布式锁
思路:
首先,对于秒杀系统中的活动页面一般是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,因此要对活动页面做静态化处理。用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端。
但只做页面静态化还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,网速各不相同。为了让用户最快访问到活动页面,这就需要使用CDN,它的全称是Content Delivery Network,即内容分发网络。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
秒杀是一个读多写少 的场景(因为在秒杀过程中,每一个请求一般都会检查库存是否充足,足够了才允许修改库存下单,由于大量用户抢少量商品,只有极少部分用户能够抢成功,所以绝大部分用户在秒杀时,库存其实是不足的,系统会直接返回该商品已经抢完。这是非常典型的:读多写少 的场景)。
如果直接使用MYSQL,这么高的并发数据库极有可能挂点,因此应该对库存信息使用redis缓存,,在秒杀活动开始前,将库存信息预缓存倒redis中,同时采用分布式锁防止缓存击穿,并且为了提高系统可用性可以使用集群的方式部署多个节点。
为防止超买超卖(query查询操作非原子操作) ,扣减库存操作中,使用redis扣减库存。redis的incr
方法是原子性的 ,可以用该方法扣减库存,:
1)先判断该用户有没有秒杀过该商品,如果已经秒杀过,则直接返回-1。2)扣减库存,判断返回值是否小于0,如果小于0,则直接返回0,表示库存不足。3)如果扣减库存后,返回值大于或等于0,则将本次秒杀记录保存起来。然后返回1,表示成功。(下述代码不足点:由于预先执行incrby,导致库存变负数,后面有回退库存时,会导致库存不准 ) 1 2 3 4 5 6 7 8 9 bool exist = redisClient.query (productId,userId);if (exist) { return -1 ; } if (redisClient.incrby (productId, -1 )<0 ) { return 0 ; } redisClient.add (productId,userId); return 1 ;
因此,最好的策略:我们都知道lua脚本,是能够保证原子性的,而redis支持lua脚本,它跟redis一起配合使用,能够完美解决上面的问题 1 2 3 4 5 6 7 8 9 10 11 12 13 StringBuilder lua = new StringBuilder(); lua.append("if (redis.call('exists', KEYS[1]) == 1) then" ); lua.append(" local stock = tonumber(redis.call('get', KEYS[1]));" ); lua.append(" if (stock == -1) then" ); lua.append(" return 1;" ); lua.append(" end;" ); lua.append(" if (stock > 0) then" ); lua.append(" redis.call('incrby', KEYS[1], -1);" ); lua.append(" return stock;" ); lua.append(" end;" ); lua.append(" return 0;" ); lua.append("end;" ); lua.append("return -1;" );
同时,为了防止redis因为宕机导致的最后数据的不一致性,需要开启redis的持久化机制。
真实的秒杀场景中,有三个核心流程:秒杀-->生成订单-->支付
。真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理 的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。
往mq发送下单消息的时候,有可能会失败。原因有很多,比如:网络问题、broker挂了、mq服务端磁盘问题等。这些情况,都可能会造成消息丢失。那么,如何防止消息丢失呢?
方法1:在生产者发送mq消息之前,先把该条消息写入消息发送表,初始状态是待处理,然后再发送mq消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。
防范2:借助mq的消息的持久化
回退库存: 如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会该被自动取消,库存+1,这种场景可以使用延时队列 ,下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。
限流:
基于nginx的限流:针对整体而言
基于请求速率的限流:通过ngx_http_limit_req_module
模块来实现,该模块支持基于固定窗口算法和令牌桶算法的限流。
固定窗口算法:
允许定义一个请求速率(如每秒允许的请求数量)和一个时间窗口。
当请求到达时,Nginx会检查在当前的时间窗口内是否已经达到了设置的速率限制。
如果请求超出了速率限制,Nginx可以配置为返回错误状态码(如503 Service Temporarily Unavailable)或者延迟处理请求。
令牌桶算法:
通过固定速率向桶中添加令牌。
每个请求都需要消耗一个令牌才能被处理。
如果桶中有足够的令牌,请求将立即被处理;如果没有令牌,则请求可以被延迟处理或拒绝。
这种算法允许一定程度的突发流量,因为桶中可以积累令牌以应对短时间内的请求峰值。
针对科技限流: - 对同一用户限流: 了防止某个用户,请求接口次数过于频繁,可以只针对该用户做限制。比如某个id,每分钟只能请求5次接口。 - 对同一ip限流 :有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这种nginx就没法识别了。这时需要加同一ip限流功能。 - 对接口限流 :别以为限制了用户和ip就万事大吉,有些高手甚至可以使用代理,每次都请求都换一个ip。这时可以限制请求的接口总次数 。这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多,达到了该接口的请求上限,而影响其他的正常用户访问该接口。看起来有点得不偿失。 - 加验证码 :相对于上面三种方式,加验证码的方式可能更精准一些,同样能限制用户的访问频次,但好处是不会存在误杀的情况。
2. SQL的优化
在平常中我是通过这样去优化慢查询的:
首先通过慢查询日志去定位慢SQL语句,使用mysqldumplow工具分析慢查询日志,找到慢SQL 1 mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
使用EXPLAIN分析SQL语句,主要看这条查询语句当中的TYPE
、key
、rows
、extra
字段,其中type
观察该SQL语句的访问类型,是否使用索引,好坏层级为system > const > eq_ref > ref > range > index >ALL
,如果是all
或index
的话,说明走了全表扫描,没有走索引或者索引失效,导致查询慢,尝试使用走索引优化,比如建立复合索引,最少优化到range
范围,更好的则尽量ref\const
接着通过key
得到实际实际的索引,观察返回的记录数rows
是否过多,如果过多则限制返回记录数,比如去除不必要的记录或者将查询分割成小范围查询
另外一点的话,如果是index,看是否能直接使用索引覆盖,即using index
1 2 3 4 5 +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | key | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | orders | ALL | NULL | 10000 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
SQL的重新可以从减少查询数据量、减少返回的记录数、拆分复杂查询出发
1 2 3 4 5 6 7 8 9 10 11 优化前: SELECT * FROM logs WHERE create_time>'2023-01-01' ; 优化后 SELECT id,message FROM logs WHERE create_time>'2023-01-01' ; 优化前(有嵌套子查询) SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount>1000 ) ;优化后(使用联合查询) SELECT u.* FROM users u JOIN orders o ON u.id=o.user_id WHERE o.amount>1000 ;
为什么大多数场景下连接查询比嵌套子查询好
嵌套子查询可能导致内部查询重复执行,并且零时表的生成和管理增加了额外的开销
联合查询通过JOIN一次性将两个表的数据关联合并,生成中间结果集,避免了重复执行。
3. 索引的优化
Mysql索引的优化
索引类型选择: MySQL支持多种索引类型,有B+tree索引、哈希索引、全文索引(FULLTEXT,Innodb和MyISAm均支持)。可以根据实际需求选择合适的索引类型,比如B+tree索引适用于要求排序、范围查询等场景;如果是经常查询单条记录,用=
情况,在这样的精确匹配查询下,使用哈希索引合适(MEMORY存储引擎) 。
选择适当的列建立聚簇索引(适当的列是指频繁查询且唯一的字段,这样对于大多数查询来说都避免了回表查询)
建立联合索引 :选择多个经常结合查询的字段建立联合索引,并且按照最左前缀匹配,选择性高的字段放在前面。后面在查询过程中能狗进行覆盖索引,这样对于大多数查询来说都避免了回表查询。
对于字符类型索引,如果经常查询,可以尝试建立前缀索引,按照区分度选择合适的长度建立
另外,当我们看到慢SQL后,不是马上去建立索引,而是看能不能优化SQL。大多数情况下,业务SQL比较复杂,很难优化,因此建立索引要参照下面的规则:
(1)索引并非越多越好,大量的索引不仅占用磁盘空间,而且还会影响insert,delete,update等语句的性能
(2)索引需要维护,因此避免对经常更新的表做更多的索引,并且索引中的列尽可能少;对经常用于查询的字段创建索引,避免添加不必要的索引
(3)数据量少的表尽量不要使用索引,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果
(4)在条件表达式中经常用到不同值较多的列上创建索引,在不同值很少的列上不要建立索引
(5)在频繁进行排序或者分组的列上建立索引,如果排序的列有多个,可以在这些列上建立联合索引,联合索引中列顺序按照选择性排列。
4. mysql死锁的排查
开启mysql的死锁日志 1 SET GLOBAL innodb_print_all_deadlock=ON;
分析死锁日志
检查SQL的执行顺序,观察事务是否因不同顺序访问相同资源导致 1 2 3 4 5 6 7 事务1 : BEGIN; UPDATE accounts SET banlance=banlance-100 WHERE Id=1 ; 锁住了id=1 UPDATE accounts SET banlance=banlance+100 WHERE Id=2 ; 尝试锁住id=2 事务2 UPDATE accounts SET banlance=banlance-100 WHERE Id=2 ; 锁住了id=2 UPDATE accounts SET banlance=banlance+100 WHERE Id=2 ; 尝试锁住id=1
检查是是否走索引,无索引或索引不当会导致锁升级为表锁或间隙锁,增大死锁概率
在MySQL种有这样的机制,线程发现死锁后,会主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。可将参数innodb_deadlock_detect
设置为on
,表示开启这个逻辑。但是它有额外负担的。每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,消耗大。
5. 数据库,如果突然掉电数据写入失败,redo log也来不及落盘,undo log 也没办法,这种情况怎么办?
针对该问题,我想到有事前的预防措施、事中的高可用架构以及事后的业务层补偿三个方面来解决:
MySQL Group Replication基于组复制的概念,并参考了MariaDB Galera Cluster和Percona XtraDB Cluster的设计。它允许数据库服务器组织成一个组,在组内的所有成员之间进行数据复制和同步,以确保数据的一致性和可用性。
多主复制:
MGR支持多主复制模式,即允许多个节点同时处理读写请求,从而提高系统的吞吐量和可靠性。
当某个成员执行写操作时,该操作会被记录并复制到组内的其他所有成员。
事务一致性:
事务的提交必须经过半数以上节点同意方可提交,以确保数据的一致性。
MGR使用Paxos算法(通过XCom基础设施实现)来确保数据库状态机在节点间的事务一致性。
故障检测与自动故障转移:
MGR具有故障检测机制,当某个成员出现故障时,系统会自动调整组的成员关系。
当某个节点发生故障时,Group Replication会自动重新配置集群,确保服务的连续性
6. 布隆过滤器说一下