网站建设与管理的试卷,网站的pdf目录怎么做的,学全屋定制设计怎么入手,福州网建公司分布式系统缓存
缓存分类 前端缓存
前端缓存包括页面和浏览器缓存#xff0c;如果是 App#xff0c;那么在 App 端也会有缓存。当你打开商品详情页#xff0c;除了首次打开以外#xff0c;后面重复刷新时#xff0c;页面上加载的信息来自多种缓存。
页面缓存属于客户端…分布式系统缓存
缓存分类 前端缓存
前端缓存包括页面和浏览器缓存如果是 App那么在 App 端也会有缓存。当你打开商品详情页除了首次打开以外后面重复刷新时页面上加载的信息来自多种缓存。
页面缓存属于客户端缓存的一种在第一次访问时页面缓存将浏览器渲染的页面存储在本地当用户再次访问相同的页面时可以不发送网络连接直接展示缓存的内容以提升整体性能。
HTML5 支持了本地存储本地存储包括 localStorage 和 sessionStorage。
localStorage 没有时间限制在同一个浏览器中只要没被手动清理数据会一直可用。sessionStorage 则和 session 的有效期内相关关闭浏览器页面后缓存会被清空。
除了本地存储HTML5 还支持离线缓存也就是 Application Cache 技术该技术可以实现应用离线的缓存在暂时断网离线后仍然可以访问页面。
Application Cache 是基于 manifest 文件实现的缓存机制浏览器会通过这个文件上的清单解析存储资源。
页面缓存一般用于数据更新比较少的数据不会频繁修改。除了页面缓存大部分浏览器自身都会实现缓存功能比如查看某个商品信息我如果要回到之前的列表页点击后退功能就会应用到浏览器缓存另外对于页面中的图片和视频等浏览器都会进行缓存方便下次查看。
前端缓存还有 App 内的缓存由于 App 是一个单独的应用各级缓存会更加复杂在 Android 和 iOS 开发中也有区别。客户端缓存是非常重要的优化手段在开发中注意避免可能导致的问题就可以。
网络传输缓存
大多数业务请求都是通过 HTTP/HTTPS 协议实现的它们工作在 TCP 协议之上多次握手以后浏览器和服务器建立 TCP 连接然后进行数据传输在传输过程中会涉及多层缓存比如 CDN 缓存等。
网络中缓存包括 CDN 缓存CDNContent Delivery Network内容分发网络实现的关键包括 内容存储 和 内容分发
内容存储就是对数据的缓存功能内容分发则是 CDN 节点支持的负载均衡。
前端请求在经过 DNS 之后首先会被指向网络中最近的 CDN 节点该节点从真正的应用服务器获取资源返回给前端同时将静态信息缓存。在新的请求过来以后就可以只请求 CDN 节点的数据同时 CDN 节点也可以和服务器之间同步更新数据。
网络缓存还包括 负载均衡中的缓存 负载均衡服务器主要实现的是请求路由也就是负载均衡功能也可以实现部分数据的缓存比如一些配置信息等很少修改的数据。
目前业务开发中大部分负载均衡都是通过 Nginx 实现的用户请求在达到应用服务器之前会先访问 Nginx 负载均衡器。如果发现有缓存信息则直接返回给用户如果没有发现缓存信息那么 Nginx 会 回源 到应用服务器获取信息。
服务端缓存
前端请求经过负载均衡落到 Web 服务器之后就进入服务端缓存服务端缓存是缓存的重点也是业务开发平时打交道最多的缓存。它还可以进一步分为 本地缓存 和 外部缓存 。
本地缓存也可以叫作 应用内缓存 比如 Guava 实现的各级缓存或者 Java 语言中使用各类 Map 结构实现的数据存储都属于本地缓存的范畴。应用内缓存的特点是随着服务重启后失效作用时间很短好处是应用比较灵活。外部缓存就是 Redis、Memchaed 等 NoSQL 存储的分布式缓存它也是在系统设计中对整体性能提升最大的缓存。但如果外部缓存使用不当则会导致缓存穿透、缓存雪崩等业务问题。
数据库缓存
经过服务端缓存以后数据其实并不是直接请求数据库持久层在数据库层面也可以有多级缓存。
在 Java 开发中一般使用 MyBatis 或者 Hibernate 作为数据库访问的持久化层这两个组件中都支持缓存的应用。
以 MyBatis 为例MyBatis 为每个 SqlSession 都创建了 LocalCacheLocalCache 可以实现查询请求的缓存 如果查询语句命中了 缓存 返回给用户否则查询数据库 并且 写入 LocalCache 返回结果给用户。在实际开发中数据库持久层的缓存非常容易出现数据不一致的情况一般不推荐使用。
在数据库执行查询语句时MySQL 会保存一个 Key-Value 的形式缓存在内存中其中 Key 是查询语句Value 是结果集。如果缓存 Key 被命中则会直接返回给客户端否则会通过数据库引擎 进行 查询并且把结果缓存起来方便下一次调用。虽然 MySQL 支持缓存但是由于需要保证一致性当数据有修改时需要删除缓存。如果是某些更新特别频繁的数据缓存的有效时间非常短带来的优化效果并不明显。
避免缓存穿透、缓存击穿、缓存雪崩
缓存穿透
缓存穿透是指业务请求穿过了缓存层落到持久化存储上。缓存被击穿以后如果请求量比较大则会导致数据库出现风险。 以双十一为例由于各类促销活动的叠加整体网站的访问量、商品曝光量会是平时的千倍甚至万倍。巨大的流量暴涨单靠数据库是不能承载的如果缓存不能很好的工作可能会影响数据库的稳定性继而直接影响整体服务。
场景
不合理的缓存失效策略
缓存失效策略如果设置不合理比如设置了大量缓存在同一时间点失效那么将导致大量缓存数据在同一时刻发生缓存穿透业务请求直接打到持久化存储层。
外部用户的恶意攻击
外部恶意用户利用不存在的 Key来构造大批量不存在的数据请求我们的服务由于缓存中并不存在这些数据因此海量请求全部穿过缓存落在数据库中将导致数据库崩溃。
解决
缓存空数据。针对数据库不存在的数据在查询为空时添加一个对应 null 的值到缓存中这样在下次请求时可以通过缓存的结果判断数据库中是否存在避免反复的请求数据库。不过这种方式需要考虑空数据的 Key 在新增后的处理。布隆过滤器。布隆过滤器是应用非常广泛的一种数据结构Bitmap可以看作是一种特殊的布隆过滤器。使用布隆过滤器可在缓存前添加一层过滤布隆过滤器映射到缓存在缓存中不存在的数据会在布隆过滤器这一层拦截从而保护缓存和数据库的安全。
缓存击穿
表现前端请求大量的访问某个热点 Key而这个热点 Key 在某个时刻恰好失效导致请求全部落到数据库上。
二八定律在任何一组东西中最重要的只占其中一小部分约 20%其余 80% 尽管是多数却是次要的因此又称二八定律。
二八定律在缓存应用中也不能避免往往是 20% 的缓存数据承担了 80% 或者更高的请求剩下 80% 的缓存数据仅仅承担了 20% 的访问流量。
由于二八定律的存在缓存击穿虽然可能只是一小部分数据失效但这部分数据如果恰好是热点数据还是会对系统造成非常大的危险。
缓存雪崩
大量的缓存数据在同一时刻失效请求全部转发到数据库将导致数据库压力过大服务宕机缓存服务不稳定比如负责缓存的 Redis 集群宕机。
出现缓存雪崩可能会直接导致大规模服务不可用因为缓存失效时导致的雪崩一方面是整体的数据存储链路另一方面是服务调用链路最终导致微服务整体的对外服务出现问题。
微服务本身就存在雪崩效应在电商场景中如果商品服务不可用最终可能会导致依赖的订单服务、购物车服务、用户浏览等级联出现故障。
避免
明确缓存集群的容量峰值通过合理的限流和降级防止大量请求直接拖垮缓存做好缓存集群的高可用以 Redis 为例可以通过部署 RedisCluster、Proxy 等不同的缓存集群来实现缓存集群高可用。
缓存稳定性
首先明确应用缓存的目的大部分缓存都是内存数据库并且可以支持非常高的 QPS所以缓存应用可以防止海量业务请求击垮数据库保护正常的服务运行。
其次在考虑缓存的稳定性时要从两个方面展开第一个是缓存的数据第二个是缓存容器也就是缓存服务本身的稳定性。
缓存命中率指落到缓存上的请求占整体请求总量的占比。缓存命中率在电商大促等场景中是一个非常关键的指标要尽可能地提高缓存数据的命中率一般要求达到 90% 以上如果是大促等场景会要求 99% 以上的命中率。
从缓存服务的层面缓存集群本身也是一个服务也会有集群部署服务可用率服务的最大容量等。在应用缓存时要对缓存服务进行压测明确缓存的最大水位如果当前系统容量超过缓存阈值就要通过其他的高可用手段来进行调整比如服务限流请求降级使用消息队列等不同的方式。
先更新数据库还是先更新缓存
数据不一致问题
缓存层和数据库存储层是独立的系统在数据更新的时候最理想的情况是缓存和数据库同时更新成功。但由于缓存和数据库是分开的无法做到原子性的同时进行数据修改可能出现缓存更新失败或者数据库更新失败的情况这时候会出现数据不一致影响前端业务。
以电商中的商品服务为例针对 C 端用户的大部分请求都是通过缓存来承载的假设某次更新操作将商品详情 A 的价格从 1000 元更新为 1200 元数据库更新成功但是缓存更新失败。这时候就会出现 C 端用户在查看商品详情时看到的还是 1000 元实际下单时可能是别的价格最终会影响用户的购买决策影响平台的购物体验。
更新缓存方式
先更新数据库再更新缓存
在写操作中先更新数据库更新成功后再更新缓存。
问题数据库更新成功以后由于缓存和数据库是分布式的更新缓存可能会失败就会出现数据库是新的但缓存中数据是旧的出现不一致的情况。
先删缓存再更新数据库
数据更新时首先删除缓存再更新数据库这样可以在一定程度上避免数据不一致的情况。
并发场景假如某次的更新操作更新了商品详情 A 的价格线程 A 进行更新时失效了缓存数据线程 B 此时发起一次查询发现缓存为空于是查询数据库并更新缓存然后线程 A 更新数据库为新的价格。
在这种并发操作下缓存的数据仍然是旧的出现业务不一致。
先更新数据库再删缓存
缓存 数据库读写的模式( Cache Aside 方案)。具体操作是读的时候先读缓存缓存没有的话那么就读数据库然后取出数据后放入缓存同时返回响应更新的时候先更新数据库数据库更新成功之后再删除缓存。
在 Cache Aside 方案中调整了数据库更新和缓存失效的顺序先更新数据库再失效缓存。
目前大部分业务场景中都应用了读写分离如果先删除缓存在读写并发时可能出现数据不一致。考虑这种情况
线程 A 删除缓存然后更新数据库主库线程 B 读取缓存没有读到查询从库并且设置缓存为从库数据主库和从库同步。
在这种情况下缓存里的数据就是旧的所以建议先更新数据库再失效缓存。当然在 Cache Aside 方案中也存在删除缓存失败的可能因为缓存删除操作比较轻量级可以通过多次重试等来解决。
缓存更新
为什么删除而不是更新缓存
删除一个数据相比更新一个数据更加轻量级出问题的概率更小。
在实际业务中缓存的数据可能不是直接来自数据库表也许来自多张底层数据表的聚合。比如上面提到的商品详情信息在底层可能会关联商品表、价格表、库存表等如果更新了一个价格字段那么就要更新整个数据库还要关联的去查询和汇总各个周边业务系统的数据这个操作会非常耗时。
从另外一个角度不是所有的缓存数据都是频繁访问的更新后的缓存可能会长时间不被访问所以说从计算资源和整体性能的考虑更新的时候删除缓存等到下次查询命中再填充缓存是一个更好的方案。
系统设计中有一个思想叫 Lazy Loading适用于那些加载代价大的操作删除缓存而不是更新缓存就是懒加载思想的一个应用。
多级缓存如何更新
多级缓存是系统中一个常用的设计比如在电商的商品信息展示中可能会有多级缓存协同。
多级缓存之间同步数据
通过消息队列通知在数据库更新后通过事务性消息队列加监听的方式失效对应的缓存。
多级缓存比较难保证数据一致性通常用在对数据一致性不敏感的业务中比如新闻资讯类、电商的用户评论模块等。
失效策略缓存过期策略
页面置换算法
缓存技术对应到操作系统中就是缓存页面的调度算法。
在操作系统中文件的读取会先分配一定的页面空间也就是Page使用页面的时候首先去查询空间是否有该页面的缓存如果有的话则直接拿出来否则就先查询页面空间没有满就把新页面缓存起来如果页面空间满了就删除部分页面方便新的页面插入。
在操作系统的页面空间中对应淘汰旧页面的机制不同有不同页面调度方法常见的有 FIFO、LRU、LFU 过期策略
FIFOFirst In First Out先进先出根据缓存被存储的时间离当前最远的数据优先被淘汰LRULeast Recently Used最近最少使用根据最近被使用的时间离当前最远的数据优先被淘汰LFULeast Frequently Used最不经常使用在一段时间内缓存数据被使用次数最少的会被淘汰。
内存淘汰策略
操作系统的页面置换算法对应到分布式缓存中就是缓存的内存淘汰策略这里以 Redis 为例。当 Redis 节点分配的内存使用到达最大值以后为了继续提供服务Redis 会启动内存淘汰策略
noeviction默认的策略对于写请求会拒绝服务直接返回错误这种策略下可以保证数据不丢失allkeys-lru这种策略操作的范围是所有 key使用 LRU 算法进行缓存淘汰volatile-lru这种策略操作的范围是设置了过期时间的 key使用 LRU 算法进行淘汰allkeys-random这种策略下操作的范围是所有 key会进行随机淘汰数据volatile-random这种策略操作的范围是设置了过期时间的 key会进行随机淘汰volatile-ttl这种策略操作的范围是设置了过期时间的 key根据 key 的过期时间进行淘汰越早过期的越优先被淘汰。
缓存过期策略
内存淘汰是缓存服务层面的操作过期策略定义的是具体缓存数据何时失效。
Redis 是 key-value 数据库可以设置缓存 key 的过期时间过期策略就是指当 Redis 中缓存的 key 过期了Redis 如何处理。
定时过期
为每个设置过期时间的 key 都需要创建一个定时器到过期时间就会立即清除。这种方式可以立即删除过期数据避免浪费内存但是需要耗费大量的 CPU 资源去处理过期的数据可能影响缓存服务的性能。
惰性过期
可以类比懒加载的策略这个就是懒过期只有当访问一个 key 时才会判断该 key 是否已过期并且进行删除操作。这种方式可以节省 CPU 资源但是可能会出现很多无效数据占用内存极端情况下缓存中出现大量的过期 key 无法被删除。
定期过期
这种方式是上面方案的整合添加一个即将过期的缓存字典每隔一定的时间会扫描一定数量的 key并清除其中已过期的 key。
合理的缓存配置需要协调内存淘汰策略和过期策略避免内存浪费同时最大化缓存集群的吞吐量。另外Redis 的缓存失效有一点特别关键那就是如何避免大量主键在同一时间同时失效造成数据库压力过大的情况。
实现一个 LRU 缓存
在 Java 语言中实现 LUR 缓存可以直接应用内置的 LinkedHashMap重写对应的 removeEldestEntry() 方法代码如下
public class LinkedHashMapExtend extends LinkedHashMap { private int cacheSize; public LinkedHashMapExtend(int cacheSize){ super(); this.cacheSizecacheSize; } Override public boolean removeEldestEntry(Map.Entry eldest) { //重写移除逻辑 if(size()cacheSize){ return true; } return false; } } LinkedHashMap 的源码实现在原生的 removeEldestEntry 实现中默认返回了 false也就是永远不会移除最“早”的缓存数据只要扩展这个条件缓存满了移除最早的数据就实现了一个 LRU 策略.
使用原生的 Map 和双向链表来实现。
import java.util.HashMap; public class LRUCache { private int cacheSize; private int currentSize; private CacheNode head; private CacheNode tail; private HashMapInteger,CacheNode nodes; class CacheNode{ CacheNode prev; CacheNode next; int key; int value; } public LRUCache(int cacheSize){ cacheSizecacheSize; currentSize0; nodesnew HashMap(cacheSize); } public void set(Integer key,Integer value){ if(nodes.get(key)null){ //添加新元素 CacheNode nodenew CacheNode(); node.keykey; node.valuevalue; nodes.put(key,node); //移动到表头 moveToHead(node); //进行lru操作 if(currentSizecacheSize) removeTail(); else currentSize; }else{//更新元素值 CacheNode nodenodes.get(key); //移动到表头 moveToHead(node); node.valuevalue; } } private void removeTail() { if(tail!null){ nodes.remove(tail.key); if(tail.prev!null) tail.prev.nextnull; tailtail.prev; } } private void moveToHead(CacheNode node){ //链表中间的元素 if(node.prev!null){ node.prev.nextnode.next; } if(node.next!null){ node.next.prevnode.prev; } //移动到表头 node.prevnull; if(headnull){ headnode; }else{ node.nexthead; head.prevnode; } headnode; //更新tail //node就是尾部元素 if(tailnode){ //下移一位 tailtail.prev; } //缓存里就一个元素 if(tailnull){ tailnode; } } public int get(int key){ if(nodes.get(key)!null){ CacheNode nodenodes.get(key); moveToHead(node); return node.value; } return 0; } } 负载均衡一致性哈希解决问题
高可用最常用的手段就是集群扩展。
缓存的集群高可用
目前 Redis 流行的集群方案有 官方 Cluster 方案、twemproxy 代理方案、哨兵模式、Codis 等方案。
缓存服务从单点扩展到集群以后会产生缓存数据的分发问题假设我们的缓存服务器有 3 台每台缓存的数据是不相同的那么在更新缓存时放置在哪台机器上呢根据 key 获取缓存时该从哪台服务器上获取这就涉及缓存的负载均衡策略。
关于缓存集群高可用的配置方式有数据同步和不同步之分。
数据同步所有节点之间数据都是一样的不同节点互为副本这种方式不需要关心缓存数据的分发实现了缓存集群的最大可用但是由于冗余了多份缓存数据会造成比较多的服务器资源浪费另外一方面在更新缓存数据时还要考虑不同节点之间的一致性。数据不同步就是每个缓存节点存储的数据不同在缓存读写时使用一定的策略进行分发。在实际开发中大部分都是应用数据不同步的方案如果需要冗余数据则可以通过缓存集群主从同步实现。
不同路由方案的扩容问题
哈希取模路由
最常见的方式是对缓存数据进行哈希典型的操作就是通过对缓存 hash缓存 Key/ 节点数量。
假设我们有 5 台缓存服务器伪代码如下
//获取缓存服务器下标 public Integer getRoute(String key){ int cacheIndex key.hashcode() % 5; return cacheIndex; } 哈希取模的方式适合对固定数量的缓存集群进行路由但是对横向扩展不友好。如果缓存机器数量发生变更过比如从 5 台服务器调整为 10 台服务器原来的缓存数据无法分配到正确机器就会出现路由不正确从而业务请求直接落到数据库上。
一致性哈希
在负载均衡策略中可以应用一致性哈希减少节点扩展时的数据失效或者迁移的情况。 一致性哈希是一种特殊的哈希算法。在使用一致性哈希算法后哈希表槽位数大小的改变平均只需要对 K/n 个关键字重新映射其中 K 是关键字的数量n 是槽位数量。然而在传统的哈希表中添加或删除一个槽位几乎需要对所有关键字进行重新映射。 一致性哈希通过一个哈希环实现Hash 环的基本思路是获取所有的服务器节点 hash 值然后获取 key 的 hash与节点的 hash 进行对比找出顺时针最近的节点进行存储和读取。
以电商中的商品数据为例假设我们有 4 台缓存服务器
A 服务器地址 hash 结果是 100B 服务器地址 hash 结果是 200C 服务器地址 hash 结果是 300D 服务器地址 hash 结果是 400
现在有某条数据的 Key 进行哈希操作得到 200则存储在 B 服务器某条数据的 Key 进行哈希操作得到 260则存储在 C 服务器某条数据的 Key 进行哈希操作得到 500则存储在 A 服务器。
一致性哈希算法在扩展时只需要迁移少量的数据就可以。例如我们刚才的例子中如果 D 服务器下线原先路由到 D 服务器的数据只要顺时针迁移到 A 服务器就可以其他服务器不受影响我们只需要移动一台机器的数据即可。
问题数据倾斜。
假设有 A、B、C 一直到 J 服务器总共 10 台组成一个哈希环。如果从 F 服务器一直到 J 服务器的 5 个节点宕机那么这 5 台服务器原来的访问都会被转移到服务器 A 之上服务器的流量可能是原来的 5 倍或者更高直到把服务器 A 打爆这时候流量继续转移到 B 服务器就出现缓存雪崩。
解决 一个方案就是添加虚拟节点对服务器节点也进行哈希操作在整个哈希环上均匀添加若干个节点。比如 a1 和 a2 都属于 A 节点b1、b2 都属于 B 节点这样在哈希时可以平衡各个节点的数据。
TreeMap 基于红黑树实现元素默认按照 keys 的自然排序排列对外开放了一个 tailMap(K fromKey) 方法该方法可以返回比 fromKey 顺序的下一个节点大大简化了一致性哈希的实现。
缓存高可用
Redis 的主从复制
集群实现依靠副本副本之间的快速数据同步–主从复制。
Redis 的主从复制可以将一台服务器的数据复制到其他节点在 Redis 中任何节点都可以成为主节点通过 Slaveof 命令可以开启复制。
数据备份通过实现主从节点之间的最终数据一致性保证数据尽量不丢失。读写分离主节点作为写节点从节点支持读请求。当主节点的系统水位不能承担前台业务请求并发量时可以将请求路由到从节点实现集群内的动态均衡。
Redis 的主从复制选举
当主节点发生故障宕机需要运维工程师手动从从节点服务器列表中选择一个晋升为主节点并且需要更新上游客户端的配置。
在 Redis 集群中依赖 Sentinel自动实现 Failover也就是自动故障转移 。
Redis Sentinel——Redis 哨兵
主从复制场景就可以依赖 Sentinel 进行集群监控。
Redis-Sentinel 是一个独立运行的进程假如主节点宕机它还可以进行主从之间的切换。主要实现了以下的功能
不定期监控 Redis 服务运行状态发现 Redis 节点宕机可以通知上游的客户端进行调整当发现 Master 节点不可用时可以选择一个 Slave 节点作为新的 Master 机器并且更新集群中的数据同步关系
Sentinel 也存在单点问题如果 Sentinel 宕机高可用也就无法实现了所以Sentinel 必须支持集群部署。
Redis Sentine 方案是一个包含了多个 Sentinel 节点以及多个数据节点的分布式架构。除了监控 Redis 数据节点的运行状态Sentinel 节点之间还会互相监控当发现某个 Redis 数据节点不可达时Sentinel 会对这个节点做下线处理如果是 Master 节点会通过投票选择是否下线 Master 节点完成故障发现和故障转移。
Sentinel 在操作故障节点的上下线时还会通知上游的业务方整个过程不需要人工干预可以自动执行。
Redis Cluster 集群
Redis Cluster 官方的集群方案是一种无中心的架构可以整体对外提供服务。 在 Redis Cluster 集群中所有 Redis 节点都可以对外提供服务包括路由分片、负载信息、节点状态维护等所有功能都在 Redis Cluster 中实现。
Redis 各实例间通过 Gossip 通信架构清晰、依赖组件少方便横向扩展有资料介绍 Redis Cluster 集群可以扩展到 1000 个以上的节点。
Redis Cluster 客户端直接连接服务器避免了各种 Proxy 中的性能损耗可以最大限度的保证读写性能。
Codis 方案 Codis 的实现和 Redis Cluster 不同是一个“中心化的结构”同时添加了 Codis Proxy 和 Codis Manager。Codis 设计中是在 Proxy 中实现路由、数据分片等逻辑Redis 集群作为底层的存储引擎另外通过 ZooKeeper 维护节点状态。 Codis 和官方的 Redis Cluster 实现思路截然不同使用 Redis Cluster 方式数据不经过 Proxy 层直接访问到对应的节点。
Redis Cluster 划分了 16384 个槽位每个节点负责其中的一部分数据都会存储槽位的信息当客户端链接时会获得槽位信息。如果需要访问某个具体的数据 Key就可以根据本地的槽位来确定需要连接的节点。
Redis Cluster 16384 个槽位。