使用缓存可以缓解大流量的压力,显着提升程序的性能。我们在使用缓存系统的时候,尤其是在大并发的情况下,经常会遇到一些“疑难杂症”。本文总结了一些使用缓存时常见的问题和解决方法,可以作为以后遇到此类问题时的参考。在设计缓存系统时也应该考虑这些常见情况。为了表述方便,本文以数据库查询缓存为例。使用缓存可以减轻数据库的压力。缓存穿透当我们使用缓存时,往往会先尝试从缓存中获取值。如果没有值,则去数据库中取值。如果数据库中没有值,则根据业务需要返回空或抛出异常。如果用户一直访问数据库中不存在的数据,比如id为-1的数据,每次请求都会先去缓存中检查,然后再去数据库中检查,造成严重的性能问题。这种情况称为缓存穿透。解决方案有以下解决方案:验证请求参数,比如用户认证验证,id的基本验证,id<=0直接拦截,如果查询发现数据库没有value,对应的key也会存储在缓存中,该值将为空。这样下一次查询就会直接从缓存中返回。但是这里key的缓存时间应该比较短,比如30s。防止这条数据以后插入数据库,但是用户获取不到。使用Bloom过滤器来确定是否已检查密钥。如果已经勾选,则不会查询数据库。缓存击穿缓存击穿是指某个key的访问量非常大,比如并发1w/s的秒杀activity。如果key在某个时间点过期,这些大请求会瞬间发送到数据库,数据库可能会直接崩溃。解决方案缓存击穿有多种解决方案,可以结合使用:对于热点数据,慎重考虑过期时间,保证key在热点期间不会过期,有的甚至可以设置为永不过期。使用互斥体(如Java的多线程锁机制),在第一个线程访问key时加锁,等待查询数据库返回,将value插入缓存后释放锁,这样后续请求就可以了直接fetched缓存中的数据就没有了。缓存雪崩缓存雪崩是指在某个时刻,多个key失效。这样就会有大量的请求无法从缓存中取到值,全部跑到数据库中去。还有一种情况,就是缓存服务器宕机了,也算是缓存雪崩。解决方案针对以上两种情况,缓存雪崩有两种解决方案:为每个key的过期时间设置一个随机值,而不是所有key都相同。使用高可用的分布式缓存集群来保证缓存的高可用,比如redis-cluster。双写不一致在使用数据库缓存的时候,读写的过程往往是这样的:读的时候,先读取缓存,如果没有缓存,直接从数据库中读取,然后取出数据放入缓存更新时,先删除缓存,再更新数据库。所谓双写不一致,是指在发生写操作(更新)时或写操作之后,数据库中的值可能与缓存中的值不同。为什么要在更新数据库之前删除缓存?因为如果先更新数据库,然后删除缓存失败,缓存中的值就会和数据库中的值不一致。但是,这并不能完全避免双写不一致的问题。假设在大并发场景下,一个线程先删除缓存,然后再去取数据库更新。这时候另外一个线程去取缓存,发现没有值,于是读取数据库,然后把数据库的旧值设置到缓存中。第一个线程更新完数据库后,数据库中包含新值,而缓存中包含旧值,因此存在数据不一致的问题。一个比较简单的解决方案是把过期时间设置得比较低,这样数据不一致的问题只存在于缓存过期之前,在一些业务场景下是可以接受的。另一种解决方案是使用队列助手。先更新数据库,再删除缓存。如果删除失败,则将其放入队列。然后另一个任务从队列中取出消息并不断重试删除相应的键。另一种解决方案是使用一个数据队列来序列化读写操作。例如,为id为n的数据创建一个队列。对于这个数据的写操作,删除缓存后,放入一个队列中;然后另一个线程过来,发现没有缓存,就把这个读操作放到这个队列中。但是这样会增加程序的复杂度,序列化也会降低程序的吞吐量,可能得不偿失。一般主流的解决方案是先删除缓存,再更新数据库。可以满足大部分需求。
