架构原则
数据量尽量少
数据在网络上传输需要时间,且不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。
- 简化秒杀页面的大小,去掉不必要的页面装修效果
- JS/CSS压缩,减少流量
请求数要尽量少
浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。因此减少请求数可以显著减少以上这些因素导致的资源消耗。
- 合并 CSS 和 JavaScript 文件,以减少请求数
路径要尽量短
所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。
每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的的话,那么整个请求的可用性是:99.9% 的 5 次方,约等99.5%。
所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。
- 将多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用。
依赖要尽量少
所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。
要减少依赖,我们可以给系统进行分级,比如 0 级系统、1 级级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,以此类推。
注意,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。
- 次级系统服务降级
不要有单点
单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。
要避免单点,关键点是避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。
- 部署配置中心,实现配置动态化
动静分离
所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。
简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有Cookie 等私密数据。
强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面,它也可能是经过 Java 系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。
- 把静态数据缓存到离用户最近的地方
- 用户浏览器
- 服务器缓存
- CDN
静态化改造就是要直接缓存 HTTP 连接
Web 代理服务器根据请求 URL,直接取出对应的 HTTP 连接而不是仅仅缓存数据。Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。选择合适的缓存框架
- Java
- Nginx
- Apache
- Varnish
流量削峰
削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。
- 排队
- 答题
- 分层过滤
减库存
基于数据库行锁
1
update goods set inventory = inventory-1 where id = #{goodsId} and inventory > 0
问题:大量锁竞争时,会影响数据库性能。
使用数据库乐观锁
1
update goods set inventory = inventory-1 where id = #{goodsId} and inventory > 0 and version = #{version}
问题:库存100,且同时只有100人抢购商品时,实际卖出的商品可能少于100。
使用redis令牌桶
预先创建n个令牌桶,n等于商品的个数。请求到来时,先获取令牌桶,只有获取了令牌桶的请求才能实际购买商品。1
2
3
4
5
6
7
8
9
10
11// 创建令牌桶
List<String> tokens = LongStream.range(0, number)
.mapToObj(index -> "spike_" + UUID.randomUUID().toString().replace("-", ""))
.collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(String.valueOf(goodsId), tokens);
// 获取令牌桶
String token = redisTemplate.opsForList().leftPop(String.valueOf(goodsId));
if (StringUtils.isEmpty(token)) {
throw new RuntimeException(String.format("商品:%s已经售空", goodsId));
}问题:实际卖出的商品可能少于100。
使用redis预减库存
1
2
3
4
5
6
7// 判断redis中库存是否小于0
Long stock = redisTemplate.opsForValue().decrement("spike_" + goodsId);
if (stock == null || stock < 0) {
// 内存标记商品已售空
goodsOverMap.put(goodsId, true);
throw new RuntimeException(String.format("商品已经售空,商品id:%s", goodsId));
}异步减库存/创建订单
1
2
3
4
5
6
7String token = redisTemplate.opsForList().leftPop(String.valueOf(goodsId));
if (StringUtils.isEmpty(token)) {
throw new RuntimeException(String.format("商品:%s已经售空", goodsId));
}
// 异步发送秒杀消息
sendSpikeMsg(goodsId, userId);
兜底方案
降级
所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。
限流
限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
拒绝服务
当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的保护方式。
优化
静态资源优化
- JS/CSS压缩,减少流量
- CDN就近访问
缓存
- 页面缓存
- 对象缓存
其它
- 隐藏秒杀地址