拆分
拆分演进
- 单机部署
- 系统所有模块部署在一台服务器,包括应用服务、缓存、数据库、文件存储等
- 各自独立
- 每个模块独立部署,能显著提高各自的性能
- 微服务-集群化-分布式
- 将系统拆分为更小粒度的微服务,每个微服务部署到集群上,由负载均衡服务器负责将流量分发到各应用服务器,提升服务的并发处理能力
- 消息队列、缓存、数据库、应用服务都集群化部署,系统升级为分布式架构
- 多集群-异地部署
- 在分布式架构的基础上,通过在多个地区部署部署多个集群,当用户访问时,通过 DNS 解析并根据用户地理位置就近分派请求到特定集群
拆分维度
可从系统,功能和读写纬度将大的系统进行拆分
- 系统维度
- 将一个大系统拆分为多个小系统,比如将电商系统拆分为商品系统,购物车系统,订单系统,优惠券系统等
- 功能维度
- 微服务化。对拆分出的一个小系统根据功能再拆分,如优惠券系统,还可拆分为建券系统,领券系统,用券系统等
- 读写维度
- 根据读写比例进行拆分,如商品系统,读的需求量比写的多很多,若将读写揉在一起,会相互影响。因此可将商品系统拆分为商品读服务,商品写服务
缓存
缓存策略
cache aside
解决的问题:缓存和 DB 的数据一致性问题
更新数据:
- 在更新完数据库后删除缓存,而不是更新数据到缓存
- 为什么不更新而是删除缓存?因为在写多读少场景中,每次写入后需要额外计算缓存数据并更新,但实际查询 qps 很低,产生了很多无用的计算成本。这种设计的本质是 lazy 懒加载思想,只有在用到(读)的时候才计算
读取数据时:
- 先读取缓存数据,如未命中缓存,则读取数据库,并写入缓存
缓存典型问题
雪崩、击穿
雪崩:大量缓存数据同时失效或 Redis 宕机造成大量请求直接访问数据库
击穿:热点数据过期,大量请求直接访问数据库。可以认为缓存击穿是缓存雪崩的一个子集。
大量缓存数据同时失效的解决方案:
- 缓存有效期增加随机值,降低大量缓存数据同时过期的概率
- 互斥锁降低并发。当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
- 同样适合缓存击穿
- 后台更新缓存。如果缓存值为空,向消息队列发条消息,消费者查询缓存,仍为空则查数据库更新缓存。本质还是降低并发。该方案也适用于缓存预热
- 同样适合缓存击穿
Redis 宕机的解决方案:
- 服务熔断。直接返回错误,保证数据库正常。等 Redis 恢复后再继续服务
- 服务限流。只将少部分请求发送到数据库进行处理。等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制
穿透
穿透:当缓存和数据库中都没有数据时,高并发下无法构建缓存数据,大量请求直接访问数据库,即认为缓存被穿透了。一般发生缓存穿透是由于数据被误删除或者被黑客恶意攻击
解决方案:
- 缓存空值
- 当多个客户端请求一条不存在的数据时,为防止多次无效的数据库访问,在第一个客户端访问时可以将这个空值也缓存起来,之后其他客户端请求时,会命中缓存中的空值,降低数据库压力
- 布隆过滤器
- 写入数据时,更新布隆过滤器。查询数据时,先查询布隆过滤器是否存在,若不存在,直接返回空,若存在,再去查询数据库
- 布隆过滤器原理:对一个 key 进行 k 个 hash 算法获取 k 个值,在比特数组中将这 k 个值散列后设定为 1,然后查的时候如果特定的这几个位置都为 1,那么布隆过滤器判断该 key 存在
- 布隆过滤器发生哈希冲突时可能会误判,判定为存在实际不存在。但如果判定不存在,则一定不存在
- Redis 的 bitmap 只支持 2^32 大小,对应到内存也就是 512MB,误判率万分之一,可以放下 2 亿左右的数据,性能高,空间占用率及小,省去了大量无效的数据库连接
- 除了用于解决缓存穿透,布隆过滤器另一个用途是用来去重。查数据库来判断 exists 的操作,在高 qps 场景中可以使用布隆过滤器,减轻 db 压力
降级
降级分类
功能降级
- 如电商平台的推荐功能,可以提升销量和转化,但不是购物的核心流程,在系统压力大时可以降级为默认的内容
服务降级
- 如在流量高峰时只更新缓存,不更新数据库,把要写入数据库的数据放到消息队列,在流量高峰过后,把消息队列中的数据更新到数据库
限流
限流也是服务降级的一种方式
限流算法请参考我的另一篇文章:系统学习流控技术。