业务场景
如何将十几秒的查询请求优化成毫秒级
缓存中间件技术选型
目前比较流行的缓存中间件Memcached、MongoDB、Redis进行简单对比

使用MongoDB的最少,因为它只是一个数据库,由于它的读写速度与其他数据库相比更快,人们才把它当作类似缓存的存储。
所以接下来就是比较Redis和Memcached,并从中做出选择。目前,Redis比Memcached更流行,这里总结一下原因,共3点。
(1)数据结构举个例子,在使用Memcached保存List缓存对象的过程中,如果往List中增加一条数据,则首先需要读取整个List,再反序列化塞入数据,接着再序列化存储回Memcached。而对于Redis而言,这仅仅是一个Redis请求,它会直接帮助塞入数据并存储,简单快捷。
(2)持久化对于Memcached来说,一旦系统宕机数据就会丢失。因为Memcached的设计初衷就是一个纯内存缓存。通过Memcached的官方文档得知,1.5.18版本以后的Memcached支持Restartable Cache(可重启缓存),其实现原理是重启时CLI先发信号给守护进程,然后守护进程将内存持久化至一个文件中,系统重启时再从那个文件中恢复数据。不过,这个设计仅在正常重启情况下使用,意外情况还是不处理。而Redis是有持久化功能的。
(3)集群这点尤为重要。Memcached的集群设计非常简单,客户端根据Hash值直接判断存取的Memcached节点。而Redis的集群因在高可用、主从、冗余、Failover等方面都有所考虑,所以集群设计相对复杂些,属于较常规的分布式高可用架构。因此,经过一番慎重的思考,项目组最终决定使用Redis作为缓存的中间件。技术选型完成后,开始考虑缓存的一些具体问题,先从缓存何时存储数据入手。
缓存何时存储数据
使用缓存的逻辑如下:
1)先尝试从缓存中读取数据。
2)若缓存中没有数据或者数据过期,再从数据库中读取数据保存到缓存中。
3)最终把缓存数据返回给调用方。
这种逻辑唯一麻烦的地方是,当用户发来大量的并发请求时,它们会发现缓存中没有数据,那么所有请求会同时挤在第2)步,此时如果这些请求全部从数据库读取数据,就会让数据库崩溃。
数据库的崩溃可以分为3种情况。
1)单一数据过期或者不存在,这种情况称为缓存击穿。
解决方案:第一个线程如果发现Key不存在,就先给Key加锁,再从数据库读取数据保存到缓存中,最后释放锁。如果其他线程正在读取同一个Key值,那么必须等到锁释放后才行。关于锁的问题前面已经讲过,此处不再赘述。
2)数据大面积过期或者Redis宕机,这种情况称为缓存雪崩。
解决方案:设置缓存的过期时间为随机分布或设置永不过期即可。
3)一个恶意请求获取的Key不在数据库中,这种情况称为缓存穿透。
比如正常的商品ID是从100000到1000000(10万到100万之间的数值),那么恶意请求就可能会故意请求2000000以上的数据。这种情况如果不做处理,恶意请求每次进来时,肯定会发现缓存中没有值,那么每次都会查询数据库,虽然最终也没在数据库中找到商品,但是无疑给数据库增加了负担。
这里给出两种解决办法:
①在业务逻辑中直接校验,在数据库不被访问的前提下过滤掉不存在的Key。
②针对恶意请求的Key存放一个空值在缓存中,防止恶意请求骚扰数据库。
缓存预热
上面这些逻辑都是在确保查询数据的请求已经过来后如何适当地处理,如果缓存数据找不到,再去数据库查询,最终是要占用服务器额外资源的。那么最理想的就是在用户请求过来之前把数据都缓存到Redis中。这就是缓存预热。其具体做法就是在深夜无人访问或访问量小的时候,将预热的数据保存到缓存中,这样流量大的时候,用户查询就无须再从数据库读取数据了,将大大减小数据读取压力。
如何更新缓存
更新缓存的步骤特别简单,共两步:更新数据库和更新缓存。但这简单的两步中需要考虑很多问题。
1)先更新数据库还是先更新缓存?更新缓存时先删除还是直接更新?
2)假设第一步成功了,第二步失败了怎么办?
3)假设两个线程同时更新同一个数据,A线程先完成第一步,B线程先完成第二步怎么办?
解决上面的三个问题,存在以下5中方案:
组合1:先更新缓存,再更新数据库
组合2:先删除缓存,再更新数据库
组合3:先更新数据库,再更新缓存
组合4:先更新数据库,再删除缓存
组合5:先删除缓存,更新数据库,再删除缓存
以上五种组合,第5种是相对最优的解决方案,可以最大程度确保数据准确性,降低查询到脏数据(这里指旧数据)的可能性。
缓存的高可用设计
设计高可用方案时,需要考虑5个要点。
1)负载均衡:是否可以通过加节点的方式来水平分担读请求压力。
2)分片:是否可以通过划分到不同节点的方式来水平分担写压力。
3)数据冗余:一个节点的数据如果失效,其他节点的数据是否可以直接承担失效节点的职责。
4)Failover:任何节点失效后,集群的职责是否可以重新分配以保障集群正常工作。
5)一致性保证:在数据冗余、Failover、分片机制的数据转移过程中,如果某个地方出了问题,能否保证所有的节点数据或节点与数据库之间数据的一致性(依靠Redis本身是不行的)。
如果对缓存高可用有需求,可以使用Redis的Cluster模式,以上5个要点它都会涉及。关于Cluster的配置方法,可以参考Redis官方文档或其他相关教程。
缓存的监控
缓存上线以后,还需要定时查看其使用情况,再判断业务逻辑是否需要优化,也就是所谓的缓存监控。在查看缓存使用情况时,一般会监控缓存命中率、内存利用率、慢日志、延迟、客户端连接数等数据。
目前也有很多开源的监控工具,如RedisLive、Redis-monitor。至于最终使用哪种监控工具,则需要根据实际情况而定。