1.11 缓存的核心知识
缓存是为了减少数据库和服务器压力而产生的,在应用层编程时需主要考虑以下几种情况:
• 客户端缓存。
• 服务端缓存。
• 网络缓存(CDN缓存)。
客户端缓存负责减轻服务端的存储和频繁的数据请求等压力。例如,在QQ初始阶段,只有“会员”才可以把QQ表情存储在“云端”之上,因为腾讯内部并没有庞大的存储系统存储大量的QQ表情。虽然现在腾讯已经取消了只有“会员”才可以存储QQ表情的限制,但是大部分QQ表情仍然默认存储在本地客户端。客户端缓存大致可分为以下几种:
• 客户端本地文件缓存,包括图片、.txt文件、.doc文件等。
• 客户端本地HTTP、cookie等浏览器缓存。
• 客户端注册表。
• 客户端微型数据库(SQLite)。
• 客户端本地计算机内存。
服务端缓存主要是为了减少数据库压力和外部服务接口的压力,这也是实际编程中最常用的手段。除减少数据库的压力外,缓存返回数据的响应速度比数据库要快。另外,尽可能不调用外部接口,因为外部接口无论WebSocket、WebService,还是HTTP,其响应速度都是不可控的。如果外部接口响应时间过长,也会影响自身性能。服务端缓存大致分为以下几种:
• 容器缓存,如Tomcat、Nginx、JBoss、Servlet等。
• 中间件缓存,如MongoDB、Elasticsearch、Redis、RocketMQ、Kafka、ZooKeeper等。
• JDK缓存,如磁盘缓存、堆内缓存、堆外缓存等。
• 页面静态化缓存,如FreeMaker、Thymeleaf等。
• 文件管理,如FastDFS等。
1.11.1 缓存的命中率
缓存的命中率指的是“缓存查询的次数”与“总查询次数”的比值。在多级缓存下,可以调研每一级缓存的命中率,以便调整代码。若某缓存命中率过低,则很可能是缓存穿透问题。
1.11.2 缓存回收方式
• 基于时间:当某缓存超过生存时间时,则进行缓存回收。或者当某缓存最后被访问后超过某时间仍然没有被访问,则进行缓存回收。
• 基于空间:当缓存超过某大小时,则进行缓存回收。
• 基于容量:当缓存超过某存储条数时,则进行缓存回收。
• 基于引用:软引用和弱引用缓存会在JVM堆内存不足时进行缓存回收。
1.11.3 缓存回收策略
• 先进先出(First In First Out,FIFO):一种简单的淘汰策略,缓存对象以队列的形式存在,如果空间不足,就释放队列头部的(先缓存)对象,一般用链表实现。
• 最近最久未使用(Least Recently Used,LRU):是根据访问的时间先后进行淘汰的,如果空间不足,就释放最久没有被访问的对象(上次访问时间最早的对象)。
• 最近最少使用(Least Frequently Used,LFU):根据最近访问的频率进行淘汰,如果空间不足,就释放最近访问频率最低的对象。
1.11.4 缓存的设计模式
(1)Cache Aside模式:首先读取缓存中的数据,若缓存没有命中,则读取DB。当DB需要更新时,直接删掉缓存中的数据。由于实现简单,因此是最常用的一种设计模式,适用于读操作多的情况。
(2)Read/Write through模式:在读取时先到缓存中查询数据是否存在。如果存在,则直接返回。如果不存在,则由缓存组件负责从数据库中同步加载数据,此数据永不过期。在写入时,先查询要写入的数据在缓存中是否存在。如果存在。则更新缓存中的数据,并且由缓存组件把数据同步更新到数据库中。Read/Write through模式初步屏蔽了底层数据库操作,但是当把数据从缓存组件写入DB时,有可能出现异常无法正确写入的情况。因而需要谨慎记录时间戳,以便跟踪维护处理数据。该方案适合对持久性要求较低的业务场景。
(3)Write Behind Caching(Write Back)模式:Write Behind Caching模式属于Read/Write through模式的进阶版,完全不考虑DB,增删改查全部通过缓存进行处理。如果读取不到数据,则直接认为该数据不存在,服务器会定期把缓存中的数据存储到DB中。一般高并发应用程序最常用的是Write Behind Caching设计模式,它是性能最好的设计模式,但是实现较为复杂,一旦服务器宕机则有可能导致大量数据丢失。
1.11.5 缓存测试应涵盖的内容
(1)当前程序是否有可能出现缓存穿透、缓存击穿、缓存雪崩等常见问题。
(2)缓存是否设置了最大位数及时间等功能,是否会出现内存溢出的现象。
(3)缓存能够节省各数据源多少比重的读取,例如进程内缓存节省了多少读取Redis的比重,Redis缓存节省了多少读取磁盘缓存的比重,磁盘缓存节省了多少读取MySQL的比重。
(4)App在无网或弱网环境下,是否可以正常打开及使用。例如网易云音乐在没有网络的情况下可以听一些本地缓存的歌曲。
(5)App在弱网转正常网络之后,缓存是否能被正常覆盖。
(6)各级缓存与数据库是否能够保持数据一致性,是否包含脏读、不可重复读等相关问题。
(7)缓存是否能够被手动删除或刷新,若遇到紧急状况是否能够进行可逆性操作。
(8)缓存的回收策略、回收方式等内容是否正常生效。
1.11.6 实战:秒杀系统设计方案
秒杀系统设计要解决的问题如下:
• 突发性大量接口请求导致服务器高负载,此时需要用限流和削峰的方案进行处理。
• 如果突然增加的带宽超过服务商提供的带宽上限,则要注意数据传输的完整性,即从客户端向服务器传输数据时即使速度缓慢,也要保证数据的完整性,以免数据丢失导致相应的错误,此时需要用队列及分布式锁等方案进行处理。
• 秒杀时应通过减库存操作维持数据的一致性,以免造成重复下单(超买/超卖)、库存不足等现象。此时需要用网关及队列等方案进行处理。
• 在秒杀之前按钮应为灰色,之后在不刷新页面的情况下将按钮点亮。此处尽量隐藏URL,并对通信信息进行加密处理,以限制各种脚本请求,尽可能按F12键后查看不到各种地址及相关信息。
• 控制刷新页面,当用户即将参与秒杀时通常会不断按F5键刷新页面,重新加载页面同样会请求接口,应减少此种接口的请求,并将部分数据缓存至客户端,减轻服务器的压力。
秒杀系统需要达到限流、削峰、异步处理、高可用、缓存、可扩展等要求,具体如下:
• 限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。常见的限流有单一端口登录(同一账号只能单一端口登录,例如App与Web只允许一个端口正在登录)、只有登录账号才能参与秒杀、当单击按钮次数过多时应限制单击次数,例如,每个账号每秒只能单击3次等不同的限流方案。
• 削峰:因为秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会出现瞬间峰值。高峰值流量是压垮系统的主要原因之一,如何把瞬间的高流量转变成一段时间的平稳的流量是设计秒杀系统很重要的思路。对流量进行削峰的解决方案是用消息队列缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。消息队列中间件主要解决应用耦合、异步消息、流量削峰等问题。常用的消息队列有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ和RocketMQ等。削峰不是一次性可以解决的方案,而是要层层削峰,每一层都把压力降到最小,再传输给下一层,这样才能接受更大的压力与并发。
• 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。异步处理的设计不仅可以削峰,还可以减轻对缓存、数据库和I/O的压力。
• 高可用:所有服务器无论应用层还是数据层都要达到高可用的标准,即任何一台服务器宕机都可由其他服务器暂时替代,并通过自动或手动的方式迅速重启服务器,保证用户几乎感受不到服务器宕机。
• 缓存:秒杀系统最大的瓶颈一般都是数据库读写。由于数据库读写属于磁盘I/O,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,则会极大地提升效率。
• 可扩展:这里讲的可扩展指一旦性能无法支撑当前并发,则可以迅速通过提升服务器性能或快速平滑地增加集群服务器的方式,顶住当前高峰压力。当压力下降时,再通过自动或手动的方式,平滑地卸下集群内部的服务器。