电商
容量规划、架构设计、数据库设计、缓存设计、框架选型、数据迁移 方案、性能压测、监控报警、领域模型、回滚方案、高并发、分库分表
# 常见术语
- PV: Page view,即网站被浏览的总次数
- UV: Unique Vister的缩写,独立访客
- CR: ConversionRate的缩写,是指访问某一网站访客中,转化的访客占全访客的比例 (订单转化率=有效订单数/访客数)
- SPU: Standard Product Unit (标准化产品单元),SPU是商品信息聚合的最小单位,是 一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
- SKU: Stock keeping unit(库存量单位) SKU即库存进出计量的单位(买家购买、商家进 货、供应商备货、工厂生产都是依据SKU进行的),在服装、鞋类商品中使用最多最普遍。 例如纺织品中一个SKU通常表示:规格、颜色、款式。SKU是物理上不可分割的最小存货单 元。
# 淘宝十年
- 1.0 : LAMP + mysql 读写分离
- 1.1 : LAP + ES + ORACLE
- 2.0 : java + 2个ORACLE + ES
- 2.1 : java(多个server) + 多个ORACLE + ES + 分布式缓存 + CDN + 分布式文件存储
- 3.0 : 分布式,拆服务了。
- 4.0 : 云原生 ,微服务、容器化部署、DevOps
# 分布式Session (opens new window)
- session Sticky : ip负载到固定服务器
- Session Replication : session 复制,开销大
- Session数据集中存储 : Session数据不保存到本机而且存放到一个集中存储的地方
- Cookie Based : 所有 session 数据放在 cookie 中
# 商品详细页
资料 -> 图片处理 -> 发布 -> 维护 -> 下架
# 表的设计
商品+分类+品牌+属性(spu级别)+规格(SKU)
# 商品展示
- 静态化处理 : FreeMarker 适合小流量架构,可以放到CDN上缓存,但是更新商品时间需要所有都替换。
- 前后端分离: vue + 接口 , 接口中加2级缓存 (jvm + redis),一般的可以实现。但是热点商品还是可能会把接口冲挂,触发熔断的操作。
- 3级缓存 : 前端缓存OpenResty(lua+nginx) + 接口2级缓存。 (京东的实现)
# 缓存
# 双写一致性
在秒杀后台把价格修改之后,如何同步到缓存中
# 最终一致性方案
设置过期时间来解决
# 实时一致性方案
- 交易canal监控binlog。
- 这个适合不频繁的操作,否则也会有性能问题。
# 对象大小
- 利用grpc的protobuffer压缩对象大小放到redis中。
- 放到redis的内存也少了
- 也减轻网络压力
- 同时也支持其他语言来读这缓存,可扩展性也强。
# 高并发缓存失效
- 现象: 加了缓存,统一时间来的数据过大,并发导致,同一时刻出现缓存失效,大量数据到数据库了
- 利用分布式锁锁住查询数据库和redis.set操作。
# 代码lock
try{
lock.lock();
// ....
}finally{
lock.unlock();
}
2
3
4
5
6
# 代码tryLock
try{
// 这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
if (lock.tryLock(0,5,SECONDS)) { // 等待时间=0,等待时间=5,时间单位SECONDS.
// 执行sql查数据
}else{
//因为 非公平的,线程可能还没给redis.set 这里只能保证可能。
//方案一: sleep 再 redis.get 递归调用
}
//方案二: TODO 改进可以改成公平的锁.或者再上面tryLock(1,sec)等一下再拿。
}finally{
// 判断被锁住,且是自己锁的。
if ( lock.isHeldByCurrentThread() && lock.isLocked() )
lock.unlock();
}
// 缺点: 2次redis的查询。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# zk锁
- 优点: 强一致性锁,解决redis集群锁同步不成功问题。
- 缺点: 性能下降
- 原理: 临时顺序节点。有序节点、临时节点、事件监听3个去中的。
- 流程: 创建一个节点下的临时顺序节点,如果不是第一个就监听上一个节点,上个节点释放就排队到前面去了。 类似于公平锁。
# 缓存击穿
- 热点数据单个key
- 前置条件: 高并发请求过来
- 定义: key过期,同一个过期的Key有大量的请求直接到DB上。
解决方案:
- 加锁 , 在未命中缓存时,通过加锁避免大量请求访问数据库
- 不允许过期,定时更新。物理不过期,也就是不设置过期时间。而是逻辑上定时在后台异步的更新数据。
- 采用二级缓存。L1缓存失效时间短,L2缓存失效时间长。二级缓存,redis发布/订阅监控set事件。
# 缓存穿透
- 恶意攻击、访问不存在数据
- 前置条件: 请求缓存和数据都没有的数据
- redis和DB都没有,造成无效请求.
解决方案:
- 布隆过滤器
- 设置空对象进行缓存,(我们采用的就是这种)但它的 过期时间会很短,最长不超过五分钟
# 缓存雪崩
- 同一时间失效,并发量大
- 前置条件: 大量的key同时失效。
- 间接的造成大量请求到达数据库。
解决方案:
- 非时点性数据(不火),使用失效的固定时间加上随机值。
- 时点性数据(热点数据),没有查到就加锁数据库查询,改成串行。
# 架构
- 事前 : 缓存集群实现高可用
- 事中 :使用Hystrix进行限流 & 降级
- 事后 : 开启Redis持久化机制,尽快恢复缓存集群
# 二级缓存
- guava的缓存使用: 1、设置最大容量 2、初始化容量 3、缓存过期
- Jetcache 框架.
# 布隆过滤器
- 项目初始化时,放到key不存在,就全量放到bitmap中.
- 增加时,MQ添加进去。
- 再接口中使用,判断并报错。
# OpenResty(3层缓存)
- 基于nginx的可用lua脚本编码的web服务器,可以说是超级高效。
- lua脚本内容 : 请求接口 + 缓存 + html模板。
- 最理想情况就是 缓存+html模板,组装html给前端。
- LRU算法:最近最少使用淘汰数据,最热的数据缓存
3层的功能
- lua+nginx:过期时间比较短(以触发LRU),数据里小、访问量相对来很高 ,超热点商品,一般存活动页、置顶的、推荐的商品。
- jvm本地缓存:过期时间比较长(防止雪崩和击穿),数据里很大、访问量相对来高 ,热点商品。
- redis:过期时间一般长,数据里相对来说比较大、访问量相对来不高,一般商品。
- db: 直接查,冷门数据。
# 多级缓存
- 浏览器缓存,当页面之间来回跳转时走local cache,或者打开页面时拿着Last-Modified 去CDN验证是否过期,减少来回传输的数据量;
- CDN缓存,用户去离自己最近的CDN节点拿数据,而不是都回源到北京机房获取数据, 提升访问性能
- 服务端应用本地缓存,我们使用Nginx+Lua架构,使用HttpLuaModule模块的shared dict做本地缓存( reload不丢失)或内存级Proxy Cache,从而减少带宽;
# 商品搜索
索引???,数据建模
# 秒杀
# 表结构
- 业务: 一个活动可以有多个场次,每个场次可以有多个商品进行秒杀
- 表: 秒杀活动表,秒杀场次表,场次商品关系表
# 业务设计
营销中心 -> 交易中心 -> 会员中心 -> 商家后台 -> 小二后台 -> 网盟系统 -> h5/app/pc -> 秒杀系统 -> 商品中心
# 流程
查询商品 -> 创建订单 -> 扣减库存 -> 更新订单 -> 付款 -> 卖家发货
# 返场
再秒杀过去了几个小时,后再去做一个返场的活动。
意义:
- 把业务上没有下单的订单取消,库存返回。
- 技术中一些服务挂了,或者其他原因导致和实际库存不一致,也有时间做技术处理。
- 也就是保证活动再久一点,尽可能把商品都卖掉,卖空。
# 防止机器刷单
- 验证码的整体设计和实现。
- 验证码不仅可以防刷,还可以有效延长下单请求的时长,更好的分散请求峰值。
- 后端验证码框架: Happy Captcha
流程:
- 从Redis判断商品是否有秒杀活动.
- 发送后台请求申请验证码。后台返回验证码图片,并将验证码的计算结果保存到Redis
- 将memberId、producrId和验证码的值一起传入后台,判断验证码的正确性,Redis中的验证码要及时删除,正确后台返回一个token。这个token会传入到接下来的商品确认页面,同时会保存到Redis当中,表示当前用户有购买秒杀商品的资格。
- 在商品确认页面,会调用确认下单接口,进行库存和上面token的校验,具体看后面的接口。
为什么要把token拼凑到请求路径,再跳页面呢??
- 为了安全,不参数传递减少了接口的暴力破解
- 同时多次跳转也增加了难度。
提前发Token优化:
秒杀前设置一个预约活动。 在活动中提前发放 token。例如一个秒杀活动有20W个商品,那就可以预先准备200W个 token。用户进行预约时,只发放200W个Token,其他人也能预约成功,但是其实没有获得token,那后面的秒杀,直接通过这个token就可 以过滤掉一大部分人。相当于没有token的人都只预约了个寂寞。这也是 互联网常用的一个套路。
这个有个问题,时间长了知道这个规律了,宣传就没效果了。所以后期改成了下面的了。
后期优化:
- 把这个token在秒杀页面做了自动获得这个token的操作。
- 同时识别手机的机器码和判断是否被root了,就免去了验证码的操作了,但是这都是移动端做的,和我没关系。
- 但是pc页面还是有验证码的操作,只是提示用app可以免验证码。
# 库存相关
- 目的地 : redis的库存,mysql的库存,还有第三方服务仓库里面登记的库存。
# 核心问题
- 并发读 : 漏斗式流量控制,对nginx、网关、jvm、redis、mysql 一层层的减少流量的到达。
- 并发写 : MQ解耦、削峰、异步.
# 主要接口
核心的处理接口:
- 价格计算: 营销中心等业务相关.
- 库存处理: 高并发下会出现超卖问题,并发技术相关。
- 以下重点针对库存的操作
# 确认下单流程
- 代码流程: 校验数量、秒杀时间、再获得信息和用户会员积分等等信息。
- 重点是校验: 校验是否有权限购买token -> 检查本地缓存售罄状态 -> 判断redis库存是否充足 -> 检查是否正在排队当中
- 目的: 第一层流量接口拦截,本地售罄状态,减少网络IO, redis库存减少网络和磁盘IO.
- 本地售罄状态在多个jvm之间同步: 不同步需要再去查redis ;用zk监听同步;用mq监听同步;用redis监听同步,性能最高(redis这种发布与订阅是没有ack的,发出去了不会管有没有收到,吞吐量相当来说就会提高,因为减少了通讯,那处理数据的能力就就会上升);
# 秒杀下单提交
- 代码流程: 检查数量(同上校验) -> 获取产品信息 -> 验证秒杀时间是否超时(同上) -> 调用会员服务获取会员信息 -> 通过Feign远程调用会员地址服务 -> 预减库存(异步流程才需要这块,数据库/分布式锁不需要此操作) -> 生成下单商品信息 -> 库存处理
# 数据库同步操作
- for update是MySQL提供的实现悲观锁的方式。获得用版本号在代码中来做乐观锁
- 性能问题: 在不考虑行锁会表锁的情况下,他还会造成,数据库和java中都有大量线程阻塞等待,2个都容易都宕机。
- 个数问题: 库存只有1个,小明下单要买2个,就会有问题,还是需要再业务层去校验。
- 架构问题: 上面说到阻塞,1000个人来抢10商品,意味着990个请求是没有意义的。
- 总结: 代码和业务简单,但是存在上面的几个问题。
- 一般高并发项目是不会在数据库加锁的,他需要刷盘和是很费性能的,数据库特别脆弱的。
# zk/redis分布锁
- 分2种,lock和trylock。
- 不管lock和tryLock重点还是里面的操作时间长短,长的可以放到MQ做异步。
- 分布式锁,底层一般都会有个子线程做续命操作。这个很费资源。
- 还是会有上面的架构问题,出现。
- gc和redis 也还是会有来不及关闭线程,导致各种崩盘。
- 总结: 也并不能解决上面的3个问题,只是把锁的操作放到了redis,用来减轻mysql的压力。
# 预售库存
- 如果先有库存就直接下单,如果没有库存就不能下单,报错返回给前端。
- 这样做可以拦截大部分流量进入到数据库中,和释放jvm的线程了。
实现:
- 但是预售库存怎么做的??? 可以用redis的原子自减(incr)操作。
- 初始化(全量):redis中的初始数据,可以再bean的初始化同步一下库存。
- 初始化(增量):分布式定时、其他接口额外处理(一般需要营销领导审批)。
- 如果没库存了,我们还可以再jvm中设置本地缓存售罄状态,这样就可以不用去访问redis了。
- 如果买了2个实际只有1个,还需要自增redis还原库存,最好还发送同步库存请求给延迟队列2分钟,去确保不会发生少卖现象。
# 库存处理
- insert 很多表,同步的话,会有性能问题。
- 所以改成MQ异步。
- 订单状态也往redis存一份,以便查询。
- 对数据库的优化有: 索引、分库分表、读写分离
好处:
- 异步下单可以分流、让服务器处理的压力变小、数据库压力减少
- 解耦的话,业务更加清晰。
- 天然的排队处理能力。
- 消息中间件有很多特性可以利用,比如订单取消。
# 处理未支付的订单
- 添加完库存相关的订单表之后再给MQ发送延迟消息。
- 再延迟消息消费端中,判断订单是否已支付,已支付就不做操作。
- 未支付就做库存的回滚操作。
# 异步订单查询接口
- 直接去redis查对应的状态,不需要jvm缓存。
- 因为有到这里已经是下单的用户了。或者说是支付成功的用户了。
- 重点要在库存操作、支付中,操作这个redis内容。
# 接口幂等性
概念咯
如果判定正确执行还是需要回滚,数据积累又如何避免??
- redis incr
- 数据库唯一主键
- 去重表
- 等等
# RocketMQ消息零丢失
- 生产端:同步发送消息、重试机制、事务消息、状态
- 服务端:刷盘存储、主从同步、 状态返回
- 消费端:pull broker offset 消费端并且返回成功 偏移值,如果消费失败那条数据
# 多种压测方案
- 线下压测,Apache ab,Apache Jmeter,这种方式是固定url压测,一般通过访问日志 收集一些url进行压测,可以简单压测单机峰值吞吐量,但是不能作为最终的压测结果,因 为这种压测会存在热点问题;
- 线上压测,可以使用Tcpcopy直接把线上流量导入到压测服务器,这种方式可以压测出机 器的性能,而且可以把流量放大,也可以使用Nginx+Lua协程机制把流量分发到多台压测 服务器
- 直接在页面埋点,让用户压测,此种压测方式可以不给用户返回内容
# 降级开关
开关前置化,如apisix,在Nginx上做开关,请求就到不了后端,减少后端压力;
spring的降级组件。Hysix,setnel。
可降级的Servlet3业务线程池隔离。
从Servlet3开始支持异步模型,Tomcat7/Jetty8开始支持。我们可以把处理过程分解为一个个的事件。通过这种将请求划分为事件方式我们可以进行更多的控制。如,我们可以为不同的业务再建立不 同的线程池进行控制:即我们只依赖tomcat线程池进行请求的解析,对于请求的处理我 们交给我们自己的线程池去完成;这样tomcat线程池就不是我们的瓶颈,造成现在无法优化的状况。通过使用这种异步化事件模型,我们可以提高整体的吞吐量,不让慢速的A 业务处理影响到其他业务处理。慢的还是慢,但是不影响其他的业务。我们通过这种机制还可以把tomcat线程池的监控拿出来,出问题时可以直接清空业务线程池,另外还可以 自定义任务队列来支持一些特殊的业务。
# 限流&降级&拒绝服务
针对秒杀系统,在遇到大流量时,更多考虑的是运行阶 段如何保障系统的稳定运行,常用的手段:限流,降级,拒绝服务。
核心思想: 流量消峰,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。 常见手段: 排队、答题、分层过滤
# 限流
- 限流就是当系统流量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
- 限流必然会导致一部分用户请求失败,因此在系统处理这种异常时一定要设置超时时间,防止因被限流的请求不能 fast fail(快速失 败)而拖垮系统。
# 限流算法
- 计算器 :
- 滑动窗口 : 根据响应速度来决定流量大小。
- 令牌桶 : 解决流量突刺
- 漏桶 : 均匀请求,排队
# 前端限流
js控制已售罄等等模式
# 接入层nginx限流
- 限制连接数
- 限制同一IP同一时间只允许有1个连接
- 限制同一IP,平均访问 和 令牌 . (用来应对平均流量和突刺的流量就排队)
# 网关限流
- gateway接入sentinel
- 持久化配置改造的坑,网关规则实体转换、 json解析丢失数据
- 维度: route维度(资源名)、自定义 API 维度(url匹配,有精确、前缀、正则)。
- 阈值:QPS和线程数。
- 流控方式: 失败和排队。
# 应用层限流
思考: 在没有事先进行缓存预热的情况下,如何避免更多的请求直接访问到数据库?
思路: 加锁?锁id吗?缓存雪崩呢?锁次数?锁多少次呢?修改代码?; 锁次数不就是信号量吗? 单机/多机 。
利用sentinel的关联DB资源,排队、阈值控制QPS。是不是就能完成上面的了??
注意编码式是基于代理的,spring代理失效的解决方案。
用sentinel的应用层限流就行了。下面大致介绍一下再应用层的模式:
- 资源名,对应springmvs的应用入口;
- 阈值,有QPS和线程数;
- 流控方式: 失败、排队、预热; 预热是一点点的流量放大。
- 流控模式: 直接、关联、链路; 关联就是以关联的为主,链路就是以起始的为主。
# 热点参数限流
缓存热点访问出现期间,应用层少数热点访问 key 产生大量缓存访问请求,冲击分布式缓存系统,大量占据 内网带宽,最终影响应用层系统稳定性;
解决方案: 二级缓存、限流。
sentinel针对不同的请求参数做不同的限制。
注意事项: 热点规则需要使用@SentinelResource("resourceName")注解;参数必须是7种基本数据类型才会生效
# 热点探测功能
上面说了二级缓存,是缓存所有redis中的数据吗??? 那肯定不是的应该只缓存热点的key,那么如何快速且准确的发现热点访问key???
- 代理redis操作或者再封装缓存的API.
- 再里面做阈值限定,每天超过1W次的key就是热点,
- 同时配合配置中心,通信模块做管理和同步。
- 热点探测一般再网关层做,公共的处理。
# 降级
降级就是当系统的容量达到一定程度时,限制(比如展示从20到5)或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。
# 分类
- 运维维度,自动化运维: 超时次数、失败次数、故障统计, 限流也算。
- 运维维度,手动开关降级 : 限制某些服务访问,灰度发布。
- 功能维度,读服务降级: 把页面静态化、缓存。
- 功能维度,写服务降级: 同步转化成异步,先写缓存(比如预售库存)。
- 系统维度,页面js降级、售罄按钮
- 系统维度,接入层降级、Nginx降级。
- 系统维度,应用层降级,编码实现是否关闭响应。
# 熔断降级
- OpenFeign整合Sentinel,feign接口配置fallbackFactory
降级规则有
- 策略: 慢调用比例、异常比例、异常数
- 最大RT和最小RT、阈值。
- 熔断时长: 用于检测恢复正常访问。
# 拒绝服务
拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。当系统负载达 到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有 效的系统保护方式。
- 利用nginx插件 (opens new window)设置过载保护
- Sentinel提供的系统规则限流
# Sentinel提供的系统规则限流
- Load 自适应(仅对 Linux/Unixlike 机器生效,默认)
- CPU usage(1.5.0+ 版本)
- 并发线程数
- 入口 QPS
# 总结
一般再网关监控
- cpu大于60%触发,扩容可能阻塞业务的对应节点。
- cpu大于80%做限流。
- 当cpu大于100%,开始做过载保护,拒绝服务请求。
服务服务之间慢服务太多,做各个请求的熔断降级。
# 分布式日志系统
见ELK中的日志系统。
# 架构组件
- Nacos 注册中心
- openfeign 服务间的调用实现
- nacos 配置中心
- gateway 网关服务
- Skywalking 链路追踪
- Elasticsearch 实时的分布式搜索和分析引擎
- Logstash 解析 Trace ID,实时数据收集引擎
- kibana 为Elasticsearch提供分析和可视化的 Web 平台
- FileBeats 收集本地日志
- Prometheus 监控采集
- Grafana 展示和报警