-
秒杀
- 秒杀业务初步分析
- 秒杀系统的挑战
- 秒杀系统设计
- 通用秒杀架构
- 页面访问
- 常见的秒杀系统架构
- 商城的秒杀系统设计和实现
- 秒杀的隔离
- 业务隔离
- 系统隔离
- 数据隔离
-
实际部署
- OpenResty
- 商品获取
- 库存获取
- Lua 访问Redis从库 — Linux 进程间通信IPC(管道、匿名管道、共享内存、信号量、消息队列、UNIX共享Socket)
- 流控
- 秒杀前期流量管控
- 预约系统设计
- 预约系统优化
- 头部电商平台策略
- 秒杀的事中流量管控
- 秒杀前期流量管控
- 削峰
- 限流
- Nginx限流
- 应用/服务层限流
- Gateway 网关流控
- 拒绝服务
- 限购、秒杀的库存与降级、热点
- 限购
- 库存扣减
- 数据库方案----性能差
- 分布式锁方案
- 高并发的扣减----降级
- 写服务降级----数据不一致问题-----Redis中扣减库存
- 读服务降级
- 简化系统功能
- 热点数据
- 读热点
- 写热点
- 防刷、风控和容灾处理
- 防刷
- 风控
- 容灾
-
大型网站高并发的读、写实践
- 高并发读写场景
- 侧重于“高并发读”的系统
- 侧重于“高并发写”的系统
- 同时有“高并发读”和“高并发写”的系统
- 高并发读写策略
- 高并发读
- 加缓存/读副本
- 并发读
- 重写轻读
- 读写分离(CQRS 架构)
- 高并发写
- 数据分片
- 异步化
- 批量写
- 高并发读
- RockDB详解
- RocksDB VS Redis
- LSM-Tree 如何兼顾读写性能
- 高并发读写场景
-
问题一:为什么需要秒杀系统? 促销,吸流
-
问题二:京东、阿里巴巴等头部电商平台都把建设秒杀系统放在了什么地位? 爆品,普通商品
-
问题三:秒杀系统对于我们意味着什么?为什么要学习秒杀系统? 高可用、高性能、高并发
秒杀
秒杀业务初步分析
秒杀系统的挑战
- 巨大的瞬时流量
- 热点数据问题
- 刷子流量 (抓包工具)
秒杀系统设计
HTTP请求所经过的链路路径
用户的一次抢购过程中,每次和系统的交互都要做什么事情
- 秒杀的活动数据: 参加秒杀活动的商品信息,主要用于商详页判断活动的倒计时、开始、结束等页面展示和抢购入口校验
- 提供结算页: 可跨平台(安卓、PC、iOS)嵌入,那么就需要提供一整套服务,包括H5页面,主要用于展示商品的抢购信息,包括商品名称、价格、抢购数量、地址、支付方式、虚拟资产等等
- 提供结算页页面渲染所需数据: 用户维度的地址、虚拟资产等数据,活动维度的名称、价格等数据
- 提供下单: 用户结算页下单,提供订单生成或是将下单数据透传给下游
DNS层: 大型网站会做一些和网络相关的防攻击措施,网络安全部门有统一的一些配置措施
Nginx层: 反向代理和负载均衡器、大流量的Web 服务器、静态资源服务器,如果把业务校验也放到这里来,就可以实现校验前置
Web服务:业务的聚合
RPC服务: 基础服务
秒杀通用架构
页面访问
- 这种实现的商详页在秒杀高并发的情况下,不做任何措施,会对后端服务,特别是产品服务和数据库造成非常大的访问压力,即使产品信息全部缓存,依然会消耗大量的后端资源和带宽
- db 没有cache,db就是小儿科
- redis 官方单机并发量是10+,扣除大小生产基本在7-8万并发量(瞬时大流量也很难应对)
- tomcat Java Web 容器性能也不行,一个请求一个线程,一个4G内存大概能启动(4000-5000)个请求
常见的秒杀系统架构
- CDN承担 Web 服务或Nginx服务提供的静态资源
- CDN是全国都有的服务器,客户端可以根据所处位置自动就近从CDN上拉取静态资源,速度更快
- Nginx的职责放大,前置用来做Web 网关,承担部分业务逻辑校验,并且可能增加黑白名单、限流和流控的功能,Nginx里写业务:
- 京东是用来做商详、秒杀的业务网关
- 美团用来做负载均衡接入层
- 12306用来做车票查询
- 充分利用Nginx的高并发、高吞吐能力
- 秒杀业务的特点,即入口流量大。但流量组成却非常的混杂,这些请求中
- 刷子请求
- 无效请求(传参等异常)
- 正常请求,一般情况下这个的比例可能是6:1:3
将真正有效的3成请求分发到下游,剩余的7成拦截在网关层。不然把这些流量都打到Web服务层,Web服务再新起线程来处理刷子和无效请求,这是种资源的浪费
商城的秒杀系统设计和实现
- 创建活动:运营人员在秒杀系统的运营后台,根据指定商品,创建秒杀活动,指定活动的开始时间、结束时间、活动库存等。
- 开启活动:活动开始之前,由秒杀系统运营后台开启秒杀,会同时往商城系统的Redis Cluster集群写入首页秒杀活动信息和往秒杀系统的Redis主从集群写诸如秒杀商品库存等信息。
- 开始抢购:用户进入到秒杀商详页准备秒杀。
- 过滤资格:商详页可以看到立即抢购的按钮,这里我们可以通过增加一些逻辑判断来限制按钮是否可以点击,比如是否设置了抢购用户等级限制,是否还有活动库存,是否设置了预约等等。如果都没限制,用户可以点击抢购按钮,进入到秒杀结算页。
- 确认订单页:在结算页,用户可更改购买数量,切换地址、支付方式等,这里的结算元素也需要按实际业务来定,更复杂的场景还可以支持积分、优惠券、红包、配送时效等,并且这些都会影响最终价格的计算。
- 支付页:确认无误后,用户提交订单,在这里后端服务可以调用风控、限购等接口,来完善校验,都通过之后,完成库存的扣减和订单的生成。
- 支付回调页:订单完成后,根据用户选择的支付方式跳转到对应的页面,比如在线支付就跳转到收银台,货到付款的话,就跳到下单成功提示页。
秒杀的隔离
- 秒杀的隔离策略:普通商品 和 秒杀商品 系统隔离
- 秒杀的隔离:业务隔离、系统隔离、数据隔离、
业务隔离
- 提报系统里进行活动提报,提供参与秒杀的商品编号、活动起止时间、库存量、限购规则、风控规则以及参与活动群体的地域分布、预计人数、会员级别等基本信息
- 技术部门就能预估出大致的流量、并发数等,并结合系统当前能支撑的容量情况,评估是否需要扩容,是否需要降级或者调整限流策略等
系统隔离
- 用户的秒杀一定是首先进入商品详情页(很多电商的秒杀系统还会在商详页进行倒计时等待,时间到了点击秒杀按钮进行抢购)。因此第一个需要关注的系统就是商品详情页,我们需要申请独立的秒杀详情页域名,独立的 Nginx负载均衡器,以及独立的详情页后端服务。
- 对域名进行隔离,可以申请一个独立的域名,专门用来承接秒杀流量,流量从专有域名进来之后,分配到专有的负载均衡器,再路由到专门的微服务分组,这样就做到了应用服务层面从入口到微服务的流量隔离
- 秒杀中流量冲击比较大的核心系统就是秒杀详情页、秒杀结算页、秒杀下单库存扣减是需要我们重点关注的对象,而相对链路末端的一些系统,经过前面的削峰之后,流量比较可控了,如收银台、支付系统,物理隔离的意义就不大,反而会增加成本。
数据隔离
- Redis缓存,一般的场景一主一从就够了,但是在秒杀场景,需要一主多从来扛读热点数据
实际部署
OpenResty
详细使用:https://blog.csdn.net/menxu_work/article/details/128400402
商品获取
OpenResty lua 静态页面
库存获取
OpenResty lua Redis
到
Lua 访问Redis从库 — Linux 进程间通信IPC(管道、匿名管道、共享内存、信号量、消息队列、UNIX共享Socket)
Nginx要通过网络访问Redis,能不能连这个网络访问都避免呢?
既然秒杀中我们会搭建Redis主从集群,让Redis的从库和Nginx在部署在同一台服务器
Nginx访问Redis时,最多经过操作系统网络协议栈的IP层即可完成数据的访问,避免了数据包在网络上的实际传输。
TCP/IP层次模型:物理层,链路层、网络层、传输层、应用层
事实上,京东内部就使用了这种设计,《亿级流量网站架构核心技术 张开涛著》就有相关的介绍,在第351页和385页
流控
秒杀前期流量管控
预约系统设计
角色
- 预约管理后台,进行活动的设置和关闭
- 预约系统向预约过的用户发短信或消息提醒
- 面向终端的预约核心微服务,提供给用户预约和取消预约能力;
数据库
- 预约活动
- 用户预约
预约系统优化
- 时间维度
- 预约人数
预约系统—具备一定程度秒杀系统的特点,即时熔断控制人数
头部电商平台策略
- 数据库分库分表,主要是用户预约关系表
- 预约历史数据,也需要有个定时任务进行结转归档,以减轻数据库的压力
预约活动信息表
- Redis 缓存里存储,如果 Redis 缓存也扛不住,可以使用Redis 一主多从来扛,也可以使用服务的本地缓存。
用户预约关系表
- 跟着用户走的,没有读热点问题,只要用户登录或者合适的时机将该用户的本次预约关系加载到Redis 缓存即可,在预约商品展示时从Redis 读取然后告诉用户是否已经预约
用户进行预约的时候怎么办呢?
- 消息中间件异步写入,做好消息的防重防丢失,同时前端提醒用户“预约排队中”
商详页展示当前预约人数给用户看,以营造商品火爆的气氛
- 自然就想到了可以在Redis 里记录一个预约人数的记录。商详页展示氛围的时候,会从Redis里获取到这个记录进行提示,而用户点击“立即预约”按钮进行预约时,会往这个key进行累加操作
一般 Redis单片也能扛个七八万的OPS。而当预约期每秒中十几万,甚至几十万预约呢?
- 本地缓存中累加,然后批量的方式写入Redis,比如累加了1000个人后一次性在Redis中incr 1000,这样就把对Redis的写压力降低了1000倍
秒杀的事中流量管控
削峰
- 流量削峰
- 验证码和问答题 demo
HappyCaptcha
- 消息队列 demo 订单
限流
1. Nginx限流
限流模块
- HttpLimitzone 限制一个客户端的并发连接数
- HttpLimitReqest 漏桶算法来限制用户的连接频率
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
location /test-limit {
default_type application/json;
limit_req zone=one burst=2 nodelay;
content_by_lua_block {
ngx.say("Test limit")
}
}
}
}
limit_req_zone 是指令名称,也就是关键字,只能在http块中使用
$binary_remote_addr
是Nginx内置绑定变量,比如$remote_port
是客户端端口号- zone=one:10m zone 是关键字,one是自定义的规则名称,后续代码中可以指定使用哪个规则;10m是指声明多大内存来支撑限流的功能,从理论上说一个1MB的区域可以包含大约16000个会话状态
- rate=1r/s rate是关键字,可以指定限流的阈值,r/s 意为每秒允许通过的请求数,我们这里就限定了每秒1请求。
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s; #同一ip不同请求地址,进入名为one的zone,限制速率为5请求/秒
limit_req_zone $binary_remote_addr $uri zone=two:10m rate=1r/s; #同一ip同一请求地址,进入名为two的zone,限制速率为1请求/秒
- limit_req 是指令名称,可在http,server, location块中使用,这个指令主要用于设置共享的内存zone和最大的突发请求大小
- zone=one 使用名为one的zone,这个zone之前使用limit_req_zone声明过
- burst=2 burst用于指定最大突发请求数,超过这个数目的请求会被延时
- nodelay 设置了nodelay,在突发请求数大于burst时,会立刻丢弃掉这部分请求,一般情况下会给客户端返回503状态
在秒杀的场景下,一般会把 rate 和 burst 设置的很低,可以都为 1,即要求1个IP 1秒内只能访问1次。
公司内用户, 在参与头部电商的秒杀活动时,最好切换到 自己的手机网络,避免被“误杀”。
2. 应用/服务层限流
- 线程池限流
- API限流
- Google提供的 RateLimiter开源包,自己手写一个基于令牌桶的限流注解和实现,在业务API代码里使用。
- Sentinel流量治理, 从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度
- 自定义限流
- 线程安全的ConcurrentLinkedQueue预先存放一批订单ID
- 定时任务刷新 (
int getCount = ORDER_COUNT_LIMIT_SECOND / (1000 / FETCH_PERIOD);
每秒2000订单100毫秒刷新获取一次一次获取2000/1000/100=200个) - 获取唯一ID起到限流作用
String orderIdStr = orderIdList.poll();
2000个订单1分钟内平均分配
- 分层过滤
- Nginx提前结束 Nginx中,启用本地缓存
lua_shared_dict stock_cache 1m; # 共享字典,也就是本地缓存,名称叫做:stock_cache,大小1m
使用ngx.shared.stock_cache get/set
- 前端:
秒杀商品已无库存,秒杀结束
- 服务层:
商品已经售罄,请购买其它商品!
- Nginx提前结束 Nginx中,启用本地缓存
3. Gateway 网关流控
-
方案一: 基于redis+lua脚本限流
gateway官方提供了RequestRateLimiter过滤器工厂,基于redis+lua脚本方式采用令牌桶算法实现了限流
-
方案二:整合sentinel限流
利用 Sentinel 的网关流控特性,在网关入口处进行流量防护,或限制 API 的调用频率。
Spring Cloud Gateway接入Sentinel实现限流的原理:
- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
Gateway 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
- 场景:对秒杀下单接口进行流控
注意:匀速排队模式暂时不支持 QPS > 1000 的场景
- 场景:商品详情接口热点参数限流
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。注意:热点规则需要使用@SentinelResource("resourceName")注解,否则不生效; 参数必须是7种基本数据类型才会生效
- 热点探测hotkey
4. 拒绝服务
Sentinel系统规则限流
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
- CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
限购、秒杀的库存与降级、热点
限购
- 商品维度限制: 不同地区
- 个人维度限制: 不单指同一用户ID,还会从同一手机号、同一收货地址、同一设备IP等维度来做限制。比如限制同一手机号每天只能下1单,每单只能购买1件,并且一个月内只能购买2件等
库存扣减
做到库存扣减的原子性和有序性。该怎么去实现它呢?
1. 数据库方案----性能差
- 行锁机制
- 悲观锁:查询和扣减放在一个事务中,在查询库存的时候使用for update,事务结束行锁释放
- 通过SQL语句:比如where语句的条件,保证库存不会被减到 0 以下
- 乐观锁
- version版本号
update set stock = stock - ? ,version = version +1 where id = ? and version = ?
- version版本号
- 数据库特性
- 直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错。
2. 分布式锁方案
- Redis
- ZooKeeper
弊端:
- 有效期问题
- NPC 红锁 网路延迟、暂停、时针
高并发的扣减----降级
降级一般是有损的,那么必然要有所牺牲,几种常见的降级:
- 写服务降级:牺牲数据─致性获取更高的性能;
- 读服务降级:故障场景下紧急降级快速止损
1. 写服务降级----数据不一致问题-----Redis中扣减库存
- 多数据源(MySQL和Redis)的场景下,数据一致性问题:除非引入分布式事务
- 流量不高的先落入MySQL数据库,再通过监听数据库的Binlog变化,把数据更新进Redis 缓存。通过缓存,我们可以扛更高流量的读操作,但是写操作仍然受制于数据库的磁盘IOPS,一般考虑一个数据库也就能支持3000~5000 TPS的写操作
- 流量激增,需要对以上的写路径进行降级,由同步写数据库降级成同步写Redis缓存、MQ异步写数据库,利用Redis 强大的OPS来扛流量,一般单个Redis 分片可达8~10万的OPS,Redis集群的OPS更高
利用Redis本身就是单线程的,解决超卖,查询和扣减需要是原子操作
PO: Lua脚本参考如下,以一行注释一行代码形式呈现:
-- -------------------Lua脚本代码开始***************************
-- 调用Redis的get指令,查询活动库存,其中KEYS[1]为传入的参数1,即库存key
local c_s = redis.call('get', KEYS[1])
-- 判断活动库存是否充足,其中KEYS[2]为传入的参数2,即当前抢购数量
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
return 0
end
-- 如果活动库存充足,则进行扣减操作。其中KEYS[2]为传入的参数2,即当前抢购数量
redis.call('decrby',KEYS[1], KEYS[2])
-- -------------------Lua脚本代码结束***********************
Redis 也挂了怎么处理
* 秒杀订单下单,采用Redis缓存中直接扣减库存,
* MQ异步保存到DB下单模式,应对高并发进行削峰,
* 如果发送消息到MQ也成为性能瓶颈,可以引入线程池,将消息改为异步发送
* 但存在着Redis宕机和本服务同时宕机的可能,会造成数据的丢失,
* 需要快速持久化扣减记录,采用WAL机制实现,保存到本地RockDB数据库
2. 读服务降级
做高可用系统设计时,要牢记就是微服务自身所依赖的外部中间件服务或者其他RPC服务,随时都可能发生故障,因此我们需要建设多级缓存,以便故障时能及时降级止损
假设当秒杀的Redis缓存出现故障时,我们就可以通过降级开关,快速将读请求降级到从Redis 缓存、MongoDB或者ES上。或者当Redis和备份缓存同时出现故障时(现实中很少出现同时故障的场景),我们还是可以通过降级开关将流量切换到数据库上,让数据库暂时承压来完成读请求服务。
3. 简化系统功能
简化系统功能就是指干掉一些不必要的流程,舍弃非核心功能
秒杀系统要求尽量简单,交互越少,数据越小,链路越短,离用户越近,响应就越快,因此非核心的功能在秒杀场景下都是可以降级的
热点数据
单位时间(1s)内,一个数据非常频繁的被访问,就可以称之为热点数据,反之可以归为一般数据或冷数据。那么单位时间内究竟多高的频次才能称为热点数据呢?实际上并没有一个明确的定义,可以根据你自己的系统吞吐能力而定。
热点商品在进行秒杀时,只有这个SKU是热点,所以再怎么进行分库分表,或者增加Redis集群的分片数,热点商品SKU落在的那个分片的能力实际并没有提升,总会触达上限,把 Redis 打挂,最后可能引发缓存击穿、系统雪崩。那我们应该怎么解决这个棘手的热点问题呢?
读热点
- 增加热点数据的副本数; 增加Redis从的副本数
- 让热点数据离用户越近越好,把热点数据再上移,在服务内部做热点数据的本地缓存
- 直接短路返回 ,某个商品秒杀的时候,这个SKU是不支持使用优惠券的,那么优惠券系统在处理的时候,可以根据商品SKU编码,直接返回空的券列表
写热点
往“预约人数”这个Redis key 上进行累加操作,当几百万人同时预约的时候,这个key就是热点写操作
- 先在JVM内存里累加,延迟提交到 Redis,这样就可以把 Redis 的OPS降低几十倍
- 库存的扣减,有一种思路,可以通过把一个热 key拆解成多个key的方式,避免热点问题。这种设计针对MySQL和Redis缓存都是适用的,但是涉及到对库存进行再细分,以及子库存挪动,非常复杂,而且边界问题比较多,容易出现库存不准的问题,需要谨慎小心的使用这种方法
- 单SKU的库存直接在Redis 单分片上进行扣减,实际上,扣减库存在秒杀链路的末端,通过我们之前的削峰和限流的各种手段,真正到库存的流量是有限的,单片的Redis OPS 能承受得了。然后,我们可以针对单SKU的库存扣减进行单独限流,保证库存单片Redis的压力。这样双管齐下,单SKU的库存Redis 扣减压力就是可控的
防刷、风控和容灾处理
防刷
- 有借助物理工具,像“金手指”这种帮忙点击手机抢购按钮
- 通过第三方软件,按时准点帮忙触发App内的抢购按钮
- 通过抓取并分析抢购的相关接口,然后自己通过程序来模拟抢购过程
快的特点:风控这种多维度校验,才能将其识别出来,除非它跳步骤
高频率的特点:防刷方案有哪些
- Nginx有条件限流,是非常简单且直接的一种方式,这种方式可以有效解决黑产流量对单个接口的高频请求,但要想防止刷子不经过前置流程直接提单,还需要引入一个流程编排的Token机制
- Token机制: 在 Nginx层做Token的生成与校验,可以做到对业务流程主数据的无侵入
比如可以通过header_filter_by_lua_block,在返回的 header里增加流程Token。Token可以做MD5,加入商品编号、活动开始时间、自定义加密key等 - 黑名单机制:本地黑名单和集群黑名单;有两个来源:一个是从外部导入,可以是风控,也可以是别的渠道;而另一个就是自力更生,自己生成自己用。
风控
不断完善用户画像的过程,而用户画像是建立风控的基础
一个用户画像的基础要素包括手机号、设备号、身份、IP、地址等,一些延展的信息还包括信贷记录、购物记录、履信记录、工作信息、社保信息等等。这些数据的收集,仅仅依靠单平台是无法做到的,这也是为什么风控的建立需要多平台、广业务、深覆盖,因为只有这样,才能够尽可能多地拿到用户数据
所谓的风控,其实就是针对某个用户,在不同的业务场景下,检查用户画像中的某些数据,是否触碰了红线,或者是某几项综合数据,是否触碰了红线。而有了完善的用户画像,黑产用户风控中的判定自然就越准。
容灾
- 同城双活
- 异地多活
大型网站高并发的读、写实践
高并发读写场景
侧重于“高并发读”的系统
- 数量级
- 响应时间
- 频率
场景:
- 搜索引擎
- 电商的商品搜索
- 电商系统的商品描述、图片和价格
侧重于“高并发写”的系统
互联网的三大变现模式:
- 游戏
- 电商
- 广告(广告扣费系统)
广告通常要么按浏览付费,要么按点击付费(业界叫作 CPC或 CPM)。具体来说,就是广告主在广告平台开通一个账号,充一笔钱进去,然后投放自己的广告。C端用户看到了这个广告后,可能点击一次扣一块钱(CPC);或者浏览这个广告,浏览1000次扣10块钱(CPM)。这里只是打个比方,实际操作当然不是这个价格。
这样的广告计费系统(即扣费系统)就是一个典型的“高并发写”的系统
- C端用户的每一次浏览或点击,都会对广告主的账号余额进行一次扣减。
- 这种扣减要尽可能实时。如果扣慢了,广告主的账户里明明没有钱了,但广告仍然在线上播放,会造成平台流量的损失
同时有“高并发读”和“高并发写”的系统
场景:
- 12306网站的火车票售卖系统
- 电商的库存系统和秒杀系统
- 支付系统和微信红包
- IM、微博和朋友圈
高并发读写策略
高并发读
1. 加缓存/读副本
-
方案1:本地缓存或集中式缓存
更新缓存的的设计模式主要有四种:Cache aside, Read through, Write through, Write behind caching:- Cache Aside Pattern: 先更新DB,后删除缓存, 后端就是一个单一的存储,而存储自己维护自己的Cache, Binlog
- Read through: 当缓存失效的时候(过期或LRU换出)
- Write Through: 在更新数据时发生
- Write Behind Caching: Linux 文件系统的 Page Cache 也是同样算法。
缓存问题:(大厂是全量缓存)
- 缓存的高可用问题:如果缓存宕机,是否会导致所有请求全部写入并压垮数据库呢?
- 缓存穿透:缓存没有宕机,但是某些Key发生了大量查询,并且这些Key都不在缓存里,导致短时间内大量请求压垮数据库。
- 缓存击穿:一个热点Key,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
- 大量的热Key过期。和第二个问题类似,也是因为某些Key失效,大量请求在短时间内写入并压垮数据库,也就是缓存雪崩。其实第一个问题也可以视为缓存雪崩。
-
方案2:MySQL的 Master/Slave
-
方案3:CDN/静态文件加速/动静分离
注意:对于Redis、MySQL的 Slave、CDN,虽然从技术上看完全不一样,但从策略上看都是一种“缓存/加副本”的形式。都是通过对数据进行冗余,达到空间换时间的效果
2. 并发读
- 方案1:异步 RPC “T1、T2、T3”
- 方案2: 冗余请求 “对冲请求”
3. 重写轻读
-
方案1:微博Feeds流的实现 “推拉结合”
-
方案2:多表的关联查询:宽表与搜索引擎
4. 总结:读写分离(CQRS 架构)
读写分离架构的典型模型
- 分别为读和写设计不同的数据结构
- 写的这一端,通常也就是在线的业务DB,通过分库分表抵抗写的压力。读的这一端为了抵抗高并发压力,针对业务场景,可能是<K,V>缓存,也可能是提前做好Join的宽表,又或者是ES搜索引擎
- 读和写的串联。定时任务定期把业务数据库中的数据转换成适合高并发读的数据结构;或者是写的一端把数据的变更发送到消息中间件,然后读的一端消费消息;或者直接监听业务数据库中的 Binlog,监听数据库的变化来更新读的一端的数据
- 读比写有延迟。因为左边写的数据是在实时变化的,右边读的数据肯定会有延迟,读和写之间是最终一致性,而不是强一致性,但这并不影响业务的正常运行
高并发写
策略1:数据分片
策略2:异步化
- 案例1:短信验证码注册或登录
- 案例2:电商的订单系统
- 案例3:广告计费系统
- 案例4:写内存 + Write-Ahead 日志
策略3:批量写
- 案例1:广告计费系统的合并扣费
- 案例2:MySQL的小事务合并机制
RockDB
在事务的实现机制上,MySQL采用的就是WAL(Write-ahead logging,预写式日志)机制来实现的,所有的修改都先被写入到日志中,然后再被应用到系统中,通常包含redo和undo两部分信息。就是我们熟知的Redo日志,文件名一般是ib_logfile0,1,2……,MySQL使用了非常复杂的机制来实现WAL,单独的日志格式,种类繁多的日志类型,分组写入等等
RocksDB是Facebook开源的一个高性能、持久化的KV存储引擎,最初是Facebook的数据库工程师团队基于Google LevelDB开发。一般来说我们很少见到过哪个项目直接使用RocksDB来保存数据,即使未来大概也不会像Redis那样被业务系统直接使用。
https://www.influxdata.com/blog/benchmarking-leveldb-vs-rocksdb-vs-hyperleveldb-vs-lmdb-performance-for-influxdb/
批量写入5000万数据,RocksDB只花了1m26.9s
RocksDB VS Redis
官方读写性能 | 实际读写性能 | 操作方式 | |
---|---|---|---|
Redis | 50万次/秒 | 10W/S | 内存 |
RocksDB | 20万次/秒 | 4W/S | 磁盘 |
RocksDB为什么能实现这么高的写入性能呢?
大多数存储系统为了能够实现快速查找都会采用树或哈希表之类的存储结构。数据在写入的时候必须写到特定的位置上。比如,我们在向B+树中写入一条数据时,必须按照B+树的排序方式,写到某个固定的节点下面。哈希表也与之类似,必须要写到特定的哈希槽中。
这样的数据结构会导致在写入数据的时候,不得不先在磁盘的这里写一部分,再到那里写一部分这样跳来跳去地写,即我们所说的“随机写”。
MySQL为了减少随机读写,下了不少功夫。而RocksDB的数据结构,可以保证写入磁盘的绝大多数操作都是顺序写入
Kafka所采用的也是顺序读写的方式,所以读写性能也非常好。凡事有利也有弊,这种数据基本上是没法查询的因为数据没有结构,只能采用遍历的方式。
RocksDB究竟如何在保证数据顺序写入的前提下,还能兼顾很好的查询性能呢?
使用了数据结构LSM-Tree
LSM-Tree 如何兼顾读写性能
LSM-Tree的全称是The Log-Structured Merge-Tree,是一种非常复杂的复合数据结构。
它包含了
- WAL (Write Ahead Log) – Mysql 预写入
- 跳表(SkipList) — Redis 跳表
- 一个分层的有序表(Sorted String Table, SSTable)
LSM-tree专门为 key-value 存储系统设计的,以牺牲部分读取性能为代价提高写入性能,通常适合于写多读少的场景