高并发热点/单点数据_性能问题解决方案

问题的描述

前段时间接了一个双11的活动,业务逻辑:**用户购买某一类商品后,活动期间的4个整点在活动页点击button领取红包,每个时段奖品数量有限,先到先得。**听着很简单,可是活动开始时,异常火热,流量超过了我们的预估,本来是分时段领取奖品,结果演变为了秒杀。

当时每个整点的QPS瞬间飙高,响应时间RT短时间内居高不下,但是整个Check下来应用全部机器的负载都非常正常,后来全面查找原因,才找到问题的根源, 是由于每个时段更新数据库同一条奖品导致超时!

整点开抢后瞬时巨量的请求同时涌入,即使我们Apache端做过初步限流,应用也做了信号量的控制,而且加上分布式缓存的使用,减缓了相当大的压力,整个业务逻辑校验阶段运作良好,但是系统的瓶颈就转移到其他环节:减奖品库存!因为我们每个时段只有一个奖品A,每次减库存都是update奖品A中的奖品余额字段!大量符合发奖要求的用户请求瞬时涌入数据库去更新此条记录,update锁行,导致后面的请求全部排队等待,等前面一个update完成释放行锁后才能处理下一个请求,大量请求等待,占用了数据库的连接!一旦数据库同一时间片内的连接数被打满,就会导致这个时间片内其他后来的全部请求因拿不到连接而超时,导致访问此数据库的其他环节也出现问题!所以RT就会异常飙高!

根据木桶理论,我们后续肯定必须得优化这个最短板,将这个瓶颈解决!针对这样的情况,我们这边出了两套方案:1、强依赖分布式缓存达到减库存的目的;2、热点/单点数据拆分,弱依赖分布式缓存,采用分散热点的方式减库存.下面请允许我详细分解下这两套方案,也希望大家提各种建设性意见!

一、强依赖分布式缓存

应用中使用分布式缓存来存储当前时间段的奖品余额,有用户中奖则将此缓存中的余额减一,不需要查询和实时更新数据库,而是每隔自定义的一段时间将缓存中的余额异步更新至数据库中。

优点:这种方式完全依赖于缓存,读写速度快,不需要实时更新数据库,降低了数据库相当大的压力;

缺点:缓存不是100%稳定,很容易丢,即使采用持久化的缓存,在高并发下有时也会出问题; 一旦丢失数据,这样就导致数据库记录的奖品余额比实际真实存在的奖品余额要多,这个时候读数据库,就会导致奖品多发,也就是所谓的超卖!

二、热点/单点数据拆分,弱依赖分布式缓存

某时段的一个奖品拆分为多条后,如何能保证先到先得的业务需求将奖品准确发完,这里就引入分布式缓存作为辅助,缓存不完全稳定没关系,只是借助其在多条奖品中进行准确分发,当数据库所有奖品都有余额的情况时,能减少查询操作!只有当某一条奖品余额为0时缓存中的数据才会失效,这时才需要查询一次数据库!

      步骤:
数组M:奖品的总数。
数字P:没有奖品的行。
数字R:有奖品的行。
如果数字R中有,但是数据库中没有,更新缓存。

      1、 同个时段的奖品拆为多份(比如10份),加行号N区分(1~10),奖1~10的数值存入数组M中;

      2、 根据行号1~10 ,查询分布式缓存中是否存在各行奖品对应的记录(缓存中存放没余额的奖品行);

            是: 将存在的行号存入数组P;

            否: 数组P值为NULL;

      3、 数组M-数组P=数组R;

      4、 判断数组R是否为空 ;

            是: 没有奖品余额,返回未中奖!

            否: 在数组R中随机一个行号L;

      5、 更新数据库表,将L行奖品余额减一;

            更新成功: 减奖品库存成功,直接返回发奖成功!

            更新失败: 极大可能原因是由于没有奖品导致

                         5.1、 查询数据库中这10个奖品List(全量list);

                         5.2、 将List中没有奖品余额的行同步至对应的缓存(第2 步),并判断List中是否所有奖品行余额全为0;

                                 是: 无奖品,返回未中奖;

                                 否: List中选择一个有余额的奖品,最好是余额最多的,将行号存入L,执行第5 步;

 

输入图片说明

优点:此方案不会发生奖品多发的情况,将单行数据分拆为多行,分散了热点,同样可以减轻数据库更新时超负荷长链等待导致的连接被等待用户占用而后续请求超时,可以通过拆分为适量的行来解决单点热点数据带来的性能问题!

缺点:此方案需要做业务拆解,增加了业务的复杂性!奖品拆分为多条,数据量太大时,不是很便捷,可能会带来数据库性能问题,但这个可以通过分库分表,旧数据迁移备份的方式解决!在奖品快被抽完的那么几微秒的用户可能存在误杀!

这就是目前针对数据库的单点热点问题,我个人的一些见解,也只是初步构想,还没有进入完全的实践中,还希望各位大神多指点一二!

秒杀问题的整体性解决方案

二 解决方案
     从上面的背景分析,解决热点数据并发更新需要注意核心问题: 减少直接对db层数据热点的并发更新,本文从业务和数据库的设计层面来规划.同时也希望大家提更好的解决思路。
1 前端层面
   前端是整个流量的入口, 正常业务访问时系统表现平稳,但是当有人恶意请求时,需要加上流控措施,比如常见的
   a 需要用户回答问题,填写验证码,移动图像等等,防止或者减少有机器人来恶意请求。 
   b 页面上采用防止机器人的判断 两秒以内的成功请求一律拒绝。
   c 通过设置nginx ,对同一个ip源的请求次数做限制,防止机器人来申请。 
  优点 有效减少或者防止有人利用机器人恶意请求
  缺点 存在一定的误杀率,错杀了正常的请求。 

2  应用层
    应用程序接收前端前端请求,进行一系列的数据库操作,在我们规避了恶意请求之后如果还是有大量的数据库写访问请求,我们需要
    a 对业务做降级

限制接口的调用次数,降低对数据库的请求压力。

选择不更新请求次数,弱化该商品申请次数的展现。类似于阅读次数,申请次数 ,与金额,库存无关的功能点。
    b 通过异步更新来避免直接写数据库 。
      应用使用分布式缓存(比如tair)来存储某项商品的申请次数或者某人的申请次数,以商品id/user_id 或者将where 条件作为key,申请试用人数为value/符合某项具体条件的 count结果为value, 有用户申请成功则更新申请试用人数。不需要查询和实时写数据库,每隔一定时间/次数将结果写入数据库。
      优点:该方法完全依赖于缓存,读写速度快,不需要实时更新数据库,减轻数据库并发写的压力;
      缺点:缓存不是100%稳定,很容易丢,即使采用持久化的缓存,在高并发下有时也可能会出现异常,穿透缓存到db ,导致前端业务展现问题。
    
3 数据库层
    a 将热点数据拆分,分在不同的库不同的表中,分散热点数据,减轻数据库并发更新热点带来的RT升高和应用连接等待时能保证业务能够正常访问其他商品表,损失局部可用性。 
    优点:实时读写数据库,前端展示数据的准确性。
    缺点:业务逻辑稍显复杂。
   b 限流补丁
     针对某些特定的sql语句 从MySQL 层面加以限制,当系统thread_running达到一定值或者某个sql执行时间超过一定阈值则拒绝该sql的执行。(阿里内部已经实现限流版本)
   c 使用MySQL的 thread pool 功能。在并发较大时,one to one的模式会引起Innodb的mutex锁争用。当前解决方法是通过innodb_thread_concurrency参数,但是该参数自身也存在锁争用,同样影响了MySQL的性能。
优点:thread pool主要从四个方面考虑:减少SQL并发,使得有足够的资源:使用线程组,独立管理:使用优先级队列,限制并发执行的事务:避免死锁。
缺点:在测试过程中发现,会有大量的连接等待kernel mutex锁,但是持续的压力会导致MySQL的thread running飙高,最终导致MySQL不可用。

解决数据库高并发访问瓶颈问题

## 一、缓存式的Web应用程序架构:

在Web层和db层之间加一层cache层,主要目的:减少数据库读取负担,提高数据读取速度。cache存取的媒介是内存,可以考虑采用分布式的cache层,这样更容易破除内存容量的限制,同时增加了灵活性。

操作步骤:

  1. 在cache中查询是否有数据,如果有,开始更新数据库,没有,直接返回。 2.更新数据库的值,如果成功,发放奖品。如果失败:先查询数据库,然后更新缓存。

二、实现MySQL数据库异步查询实现:

通常情况下在PHP中MySQL查询是串行的,如果能实现MySQL查询的异步化,就能实现多条SQL语句同时执行,这样就能大大地缩短MySQL查询的耗时,提高数据库查询的效率。目前MySQL的异步查询只在MySQLi扩展提供,查询方法分别是:

1、使用MYSQLI_ASYNC模式执行mysqli::query

2、获取异步查询结果:mysqli::reap_async_query

使用mysql异步查询,需要使用mysqlnd作为PHP的MySQL数据库驱动。

使用MySQL异步查询,因为需要给所有查询都创建一个新的连接,而MySQL服务端会为每个连接创建一个单独的线程进行处理,如果创建的线程过多,则会造成线程切换引起系统负载过高。Swoole中的异步MySQL其原理是通过MYSQLI_ASYNC模式查询,然后获取mysql连接的socket,加入到epoll事件循环中,当数据库返回结果时会回调指定函数,这个过程是完全异步非阻塞的。

三、MySQL主从读写分离:

当数据库的写压力增加,cache层(如Memcached)只能缓解数据库的读取压力。读写集中在一个数据库上让数据库不堪重负。使用主从复制技术(master-slave模式)来达到读写分离,以提高读写性能和读库的可扩展性。读写分离就是只在主服务器上写,只在从服务器上读,基本原理是让主数据库处理事务性查询,而从数据库处理select查询,数据库复制被用于把事务性查询(增删改)导致的改变更新同步到集群中的从数据库。

MySQL读写分离提升系统性能:

1、主从只负责各自的读和写,极大程度缓解X锁和S锁争用。

2、slave可以配置MyISAM引擎,提升查询性能以及节约系统开销。

3、master直接写是并发的,slave通过主库发送来的binlog恢复数据是异步的。

4、slave可以单独设置一些参数来提升其读的性能。

5、增加冗余,提高可用性。

实现主从分离可以使用MySQL中间件如:Atlas

四、分表分库:

在cache层的高速缓存,MySQL的主从复制,读写分离的基础上,这时MySQL主库的写压力开始出现瓶颈,而数据量的持续猛增,由于MyISAM使用表锁,在高并发下会出现严重的锁问题,大量的高并发MySQL应用开始使用InnoDB引擎代替MyISAM。采用Master-Slave复制模式的MySQL架构,只能对数据库的读进行扩展,而对数据的写操作还是集中在Master上。这时需要对数据库的吞吐能力进一步地扩展,以满足高并发访问与海量数据存储的需求。

对于访问极为频繁且数据量巨大的单表来说,首先要做的是减少单表的记录条数,以便减少数据查询所需的时间,提高数据库的吞吐,这就是所谓的分表【水平拆分】。在分表之前,首先需要选择适当的分表策略,使得数据能够较为均衡地分布到多张表中,并且不影响正常的查询。

分表能够解决单表数据量过大带来的查询效率下降的问题,但是却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展Slave服务器都是没有意义的,对数据库进行拆分,从而提高数据库写入能力,即分库【垂直拆分】。

分库分表的理由策略如下:

1、中间变量=user_id % ( 库数量 * 每个库的表数量 )

2、库=取整(中间变量 / 每个库的表数量)

3、表=中间变量 % 每个库的表数量

数据库经过业务拆分及分库分表,虽然查询性能和并发处理能力提高了。但是原本跨表的事务上升为分布式事务;由于记录被切分到不同的库和不同的表中,难以进行多表关联查询,并且不能不指定路由字段对数据进行查询。且分库分表后需要进一步对系统进行扩容(路由策略变更)将变得非常不方便,需要重新进行数据迁移。

转载于:https://my.oschina.net/u/3421984/blog/1611386

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值