当代三高互联网架构:
高并发:能够同时处理很多用户请求
高性能:处理用户的请求速度要快
高可用:系统要在趋近 100% 的时间内都能正确提供服务
需要关注的问题
1. 参与秒杀的商品属于热点数据,我们该如何处理热点数据?
2. 商品的库存有限,在面对大量订单的情况下,如何解决超卖的问题?
3. 如果系统用了消息队列,如何保证消息队列不丢失消息?
4. 如何保证秒杀系统的高可用?
5. 如何对项目进行压测?有哪些工具?
6. ......
热点数据
分类
根据热点数据的特点,我们通常将其分为两类:
①静态热点数据 :可以提前预测到的热点数据比如要秒杀的商品。
②动态热点数据 : 不能够提前预测到的热点数据,需要通过一些手段动态检测系统运行情况产生。
如何找到热点数据
①京东的hotkey中间件探测热点数据
开源代码:hotkey: 京东App后台中间件,毫秒级探测热点数据,毫秒级推送至服务器集群内存,大幅降低热key对数据层查询压力 (gitee.com)
②Redis 在 4.0 版本之后添加了hotkeys查找特性,可以直接利用 redis-cli --hotkeys
获取当前 keyspace 的热点 key,实现上是通过 scan
+ object freq
完成的。该方案无需二次开发,能够直接利用现成的工具,但由于需要扫描整个 keyspace,实时性上比较差,另外扫描耗时与 key 的数量正相关,如果 key 的数量比较多,耗时可能会非常长。
③代理层收集
如果所有的 Redis 请求都经过代理的话,可以考虑改动 Proxy 代码进行收集,思路与客户端基本类似。该方案对使用方完全透明,能够解决客户端 SDK 的语言异构和版本升级问题,不过开发成本会比客户端高些
④客户端收集(暴力)
改动 Redis SDK,记录每个请求,定时把收集到的数据上报,然后由一个统一的服务进行聚合计算。方案直观简单,但没法适应多语言架构,一方面多语言 SDK 对齐是个问题,另外一方面后期 SDK 的维护升级会面临比较大的困难,成本很高
如何处理热点数据
热点数据一定要放在缓存中,并且最好可以写入到 jvm 内存一份(多级缓存),并 设置个过期时间。需要注意写入到 jvm 的热点数据不宜过多,避免内存占用过大,一定要设置到淘汰策略。
注:最热点数据放入JVM是因为放在jvm内存中的数据访问速度是最快的。
流量削峰
消息队列
高峰期流量过大,可以利用消息队列减轻系统压力,慢慢处理请求,避免系统崩溃。
回答问题/验证码
我们可以在用户发起秒杀请求之前让其进行答题或者输入验证码。 这种方式一方面可以避免用户请求过于集中,另一方面可以有效解决用户使用脚本作弊。
回答问题/验证码这一步建议除了对答案的正确性做校验,还需要对用户的提交时间做校验,比如提交时 间过短(<1s)的话,大概就是使用脚本来处理的。
高可用
集群化
如果我们想要保证系统中某一个组件的高可用,往往需要搭建集群来避免单点风险,比如说 Nginx 集 群、Kafka 集群、Redis 集群。
拿 Redis 来举例说明。如果我们需要保证 Redis 高可用的话,该怎么做呢? 直接通过 Redis replication(异步复制) 搞个一主(master)多从(slave)来提高可用性和读吞吐量, slave 的多少取决于读吞吐量。
这样的方式有一个问题:一旦 master 宕机,slave 晋升成 master,同时需要修改应用方的主节点地 址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。 不过,这个问题我们可以通过 Sentinel(哨兵) 来解决。Redis Sentinel 是 Redis 官方推荐的高可用性 (HA)解决方案。 Sentinel 是 Redis 的一种运行模式 ,它主要的作用就是对 Redis 运行节点进行监控。当 master 节点出 现故障的时候, Sentinel 会帮助我们实现故障转移,确保整个 Redis 系统的可用性。整个过程完全自 动,不需要人工介入。
限流
限流是从用户访问压力的角度来考虑如何应对系统故障。限流为了对服务端的接口接受请求的频率进行 限制,防止服务挂掉。
🌰 举个例子:我们的秒杀接口一秒只能处理 10w 个请求,结果秒杀活动刚开始一下子来了 15w 个请 求。这肯定不行啊!我们只能通过限流把 5w 个请求给拦截住,不然系统直接就给整挂掉了!
限流的话可以直接用 Redis 来做(建议基于 Lua 脚本),也可以使用现成的流量控制组件比如 Sentinel 、Hystrix 、Resilience4J 。
Sentinel 主要以流量为切入点,提供 流量控制、熔断降级、系统自适应保护等功能来保护系统的稳定性 和可用性。
降级
降级是从系统功能优先级的角度考虑如何应对系统故障。 服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降 级,以此释放服务器资源以保证核心任务的正常运行。降级的核心思想就是丢车保帅,优先保证核心业务。
熔断
熔断可以防止因为秒杀交易影响到其他正常服务的提供
🌰 举个例子: 秒杀功能位于服务 A 上,服务 A 上同时还有其他的一些功能比如商品管理。如果服务 A 上的商品管理接口响应非常慢的话,其他服务直接不再请求服务 A 上的商品管理这个接口,从而有效避免其他服务被拖慢甚至拖死。
一致性
减库存
一般情况下都是 下单减扣库存 ,像现在的购物网站比如京东都是这样来做的。 不过,我们还会对业务逻辑做进一步优化,比如说对超过一定时间不付款的订单特殊处理,释放库存。 对应到代码层面,我们应该如何保证不会超卖呢? 我们上面也说,我们一般会提前将秒杀商品的信息放到缓存中去。我们可以通过 Redis 对库存进行原子操作。伪代码如下:
// 第一步:先检查库存是否充足
Integer stockNum = (Integer)redisTemplate.get(key);
if(stockNum<1){
...
}
// 第二步:如果库存充足,减少库存(假设只能购买一件)
Long count = redisTemplate.increment(key,-1);
if(count>=0){
...
}else{
...
}
你也可以通过 Lua 脚本来减少多个命令的网络开销并保证多个命令整体的原子性。伪代码如下:
-- 第一步:先检查库存是否充足,库存不足,返回0
local stockNum=tonumber(redis.call("get",key));
if stockNum<1 then
return 0;
-- 第二部:如果库存充足,减少库存(假设只能购买一件),返回1
else
redis.call('DECRBY',key,1);
return 1;
end
接口幂等
在分布式系统中,幂等(idempotency)是对请求操作结果的一个描述,这个描述就是不论执行多少次相同的请求,产生的效果和返回的结果都和发出单个请求是一样的。
🌰 举个例子:假如咱们的前后端没有保证接口幂等性,我作为用户在秒杀商品的时候,我同时点击了 多次秒杀商品按钮,后端处理了多次相同的订单请求,结果导致一个人秒杀了多个商品。这个肯定是不能出现的,属于非常严重的 bug 了!
保证分布式接口的幂等性对于数据的一致性至关重要,特别是像支付这种涉及到钱的接口。保证幂等性 这个操作并不是说前端做了就可以的,后端同样要做。 前端保证幂等性的话比较简单,一般通过当用户提交请求后将按钮致灰来做到。后端保证幂等性就稍微 麻烦一点,方法也是有很多种,比如:
1. 同步锁;
2. 分布式锁;
3. 业务字段的唯一索性约束,防止重复数据产生。
4. ......
拿分布式锁来说,我们通过加锁的方式限制用户在第一次请求未结束之前,无法进行第二次请求。 分布式锁一般基于 Redis 来做比较多一些,这也是比较推荐的一种方式。另外,如果使用 Redis 来实 现分布式锁的话,比较推荐基于 Redisson。
相关阅读:分布式锁中的王者方案 - Redisson (qq.com)
当然了,除了 Redis 之外,像 ZooKeeper 等中间也可以拿来做分布式锁。
性能测试
上线之前压力测试是必不可少的。推荐 4 个比较常用的性能测试工具:
1. Jmeter :Apache JMeter 是 JAVA 开发的性能测试工具。
2. LoadRunner:一款商业的性能测试工具。
3. Galtling :一款基于 Scala 开发的高性能服务器性能测试工具。
4. ab :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。
注意事项
上面涉及到知识点还蛮多的,如果面试官单独挑出一个来深挖还是能够问出很多问题的。
比如面试官想在消息队里上进行深挖,可能会问:
常见消息队列的对比
如何保证消息的消费顺序?
如何保证消息不丢失?
如何保证消息不重复消费?
如何设计一个消息队列?
......
再比如面试官想在 Redis 上深挖的话,可能会问:
Redis 常用的数据结构了解么?
Redis 如何保证数据不丢失?
Redis 内存占用过大导致响应速度变慢怎么解决?
缓存穿透、缓存雪崩了解么?怎么解决?
.....