高并发的读写场景解析
不管什么系统,都是做读和写。
侧重”高并发读”的系统
存储引擎 ES,solr等
终端用户-搜索(读)-> 搜索引擎 <-发布(写)- 内容生产者
- 数量级的差别:终端用户成千上百万个。生产者是公司内部产生,数量级不会太高。
- 响应时间。读的响应时间要求在毫秒级。
侧重”高并发写”的系统
广告位竞拍,扣费类的。如baidu的业务模式,抖音的广告投放扣费等。
- 按照浏览次数,点击收费。需要写的实时性。C端每次浏览点击都要进行主账号上的扣减。
同时有 “高并发读” 和 “高并发写”
- 电商的库存系统和秒杀系统
- 支付系统,微信红包
- IM,微博朋友圈
不同场景面面对的高并发压力不同,应对高并发读和高并发写的策略是不同的。
高并发读常见解决策略
本地缓存或集中式缓存
本地Map缓存
Redis缓存
- 集中式缓存需要避免的问题
- 缓存高可用
- 缓存穿透
- 缓存击穿
- 大量热key过期
读副本
Mysql Master/Slave 增加一堆Slave(网易的Mysql用法)几百台Mysql slave树。
CND/静态文件加速,动静分离
- 静态内容,数据不变。html,js,css,图片等,是静态,分发到CDN
- 动态内容,用户的信息,实时查询的数据,这些放在服务器进行处理。
并发读
异步RPC
串行读 -> 并发读
- 在链路上的请求 并发执行。
- 冗余请求 Jeaf Dean 写的 The Tail at scale。肠胃耗时优化的经验。
案例:一个用户的请求需要100台服务器联合处理,每个服务器有1%的概率发生调用延迟(1秒延迟)。那么C端用户来说,响应时间
大于1秒的概率是63%
怎么算出来的呢?如果用户请求响应时间小于1s那么 100台服务器响应时间都小于1s。这个概念是100个99%相乘。
所以是 1- 0.99 100次方。 问题就很严重。
此问题的解决方法:冗余请求。客户端同时向多台服务器发送请求,哪个返回快就用哪个,其他的丢弃。这种方法系统的调用量会翻倍。
调整一下:如果客户端在一定时间内没有收到服务端的响应,则马上给另一台或者多台发送同样的请求。客户端等待第一个响应到达之后,立即终止其他请求的处理
“一定时间” 定义为 : 内部服务95%请求的响应时间。这种方法称之为 “对冲请求”。
测试数据:采用这种方法,可以仅用2%的额外请求将系统99.9%的请求响应时间从1800ms降低到74ms.
牛逼的 The Tail at Scale
另一个方法:
捆绑请求。 上游对下游服务器进行探测,找负载低的。在请求的时候顺便探测。
核心:用更多的机器来减少延迟,扛起高并发读
重写轻读
微博 feeds流的实现
Feeds流 关注的n个人的微博进行排序成一个列表。有变更,冗余保存起来。
为每个人增加一个Feeds流,叫做收件箱。
将复杂的读逻辑,通过重写的方式,进行了简化。
实现的方案:
Redis list 缓存。并对list大小进行限制。并且持久化,分库分表。根据业务进行设置分片键。
以上解决了读的高并发,又引来一个问题:假设一个用户的粉丝很多,每个粉丝的邮箱都复制一份,计算
和延迟会很大。比如某个明星有粉丝8000万,如果复制8000万份,对系统来说是个沉重的负担。
解决方法:回到最初的思路,在读的时候进行实时聚合。用’拉’的方式进行获取。
将粉丝用户分组,份成在线和不在线。只推送在线的粉丝。系统维护一个全局的,在线列表
对于读的一端一个用户关注的人当中。有的人是推给他的。有的人需要他去拉的,需要把两者聚合起来,再按时间排序,然后分页显示,这就是’推拉结合’。
多表的关联查询:宽表于搜索引擎
多表关联到情况下,通过加Slave解决。这种方法在没有分库的情况下可以实现。
如果已经分库了,那需要多个查询进行聚合,无法使用原声的Join功能。只能从程序中分别从两个库读取,再做聚合。
存在一个问题:如果需要把聚合出来的数据按某个维度进行排序分页,这个维度是临时计算出来的维度,而不是数据库本来就有的维度。
由于无法使用数据库的排序和分页功能,也无法再内存中通过实时计算来实现排序、分页此时如何处理?
采用类似微博重写轻读的思路:提前把关联数据计算好,存在一个地方,读的时候直接去读聚合好的数据,而不是读的时候去做join.
具体的操作:准备一张宽表,把关联表的数据算好后保存在宽表里。依据实际情况,定时计算,也可能任何一张原始表发生变化时进行宽表数据的计算。
也可以使用ES类的搜索引擎来实现:把多个表的join结果做成一个个的文档,放在搜索引擎里。可以林火实现排序和分页查询。
总结
读写分离Command Query Responsiblility Spareation
分别为读和写设计不同的数据结构。在C端,当同时面临读和写的高并发压力时,把系统分成读和写两个视角来设计,各自设计适合搞并发和读写的数据结构或模型。
缓存其实是读写分离的一个简化,或者是说特例,写业务DB和读缓存用了基本一样的数据结构。
高并发写常见解决策略
数据分片
数据分片后利用多台机器的资源进行分发写操作。
MySQL: 分库分表
Redis: Redis Cluster集群
ES: 分布式索引 sharder
10亿个网页或商品分成n份,建成n个小的索引。一个查询请求来了,并行地在n各索引上查询。
异步化
异步化,无处不在。
发送请求立即响应返回。客户端轮询或者其他方式进行获取结果。
客户端发起一个Http请求,不等结果,立即发送2,3个。数据库的事务提交Write-aheadLog 。
案例
- 电商系统的拆单功能
- 短信验证码或者登录
- 写内存 + Write-Ahead 日志。
MySQL为了提高磁盘IO性能,使用了 Write-Ahead日志。也就是RedoLog。
高并发扣减MySQL中的账户余额,或者电商系统中扣减库存,直接在数据库中口,数据库可能扛不住。
可以在Redis中先扣,然后同时落地一条日志(日志可以在一个高可靠的消息中间件或数据库中插入一条条日志)。
当Redis宕机,把所有的日志重放完毕,再用数据库中的数据初始化Redis的数据。
批量写
广告计费的合并扣费
假设有10个用户,对于1个广澳,每个用户点了1次,就意味着同1个广告的主账号要扣10次钱,每次扣1块(假设点击1次扣1次)如果改成
合并扣费,就是1次扣10块钱。
扣费模块一次性从持久化消息队列中取多条消息,对多条消息按照广告的主账号进行分类然后进行扣费。
MySQL的小事务合并机制
MySQL内核会自动合并小事务进行批量的事务操作。
Canal里 进行更新的时候,进行合并事务执行。
侧重 ‘高并发写’ 的系统
扣费系统:
商城秒杀中RocketDB数据详解
异步下单 Redis挂了,但是单没有正常发出去。这时候哪里去找单呢?
引入写内存+ write ahead日志。将日志写到RocketDB