缓存雪崩一般形容的是,高并发环境中,某一个数据,因为缓存过期,读不到数据而从数据库读取,一下子大量请求堆积到了数据库上。而数据库的并发能力有限,最终导致获取不到数据库连接,读不了数据库。
模拟缓存雪崩
修改MySQL最大连接数
缓存雪崩,最终结果并不是缓存的访问出问题,而是数据库,以MySQL为例,可以通过修改数据库的最大连接数。
修改my.inf配置文件max_connections=5
创建模拟并发运行环境
Spring配置数据源和JdbcTemplate,这里就不贴出来了。创建Dao和Service类,这两个类其实是很简单的,从数据库读取数据并算出总额,因为数据量比较大(10万条记录),响应的时间相对较长超过200ms。
创建测试类,这个测试类是模拟多个线程同时请求orderService.getAmount()
这个方法。修改count值可以设定并发数量。
运行测试类后出现too many connnections的异常,说明数据库的并发请求达到瓶颈。
整个执行的过程:
- 往redis放置一个数据,设置过期时间为1秒
- 启动3000个线程,同时请求一个接口,获取总额数量
- 获取总额时,先从redis获取,如果获取不到,则从数据库读取。
redis的访问速度非常快,很有可能这3000个线程执行完毕,缓存中的数据都还没过期,这样就达不到效果,为了在并发访问过程中,缓存能刚好在这时间段里过期。有两种方法:增加并发数量,或者缩短缓存过期时间和并发启动的时间差。所以在test()方法执行开始,先sleep500毫秒。
缓存雪崩的原因
在缓存过期一刹那,有大量的线程从缓存获取数据,获取不到->从数据库读取数据->放入缓存中。这个过程虽然很短暂,但是在数据还未被重新放入缓存之前,其他线程都是要走数据库获取这一步。在高并发环境中,数据库一下子堆积了大量请求,处理不过来,最终导致数据访问失败。
解决方法
设置分散的缓存过期时间
对不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。这种情况只针对于在某个时刻往缓存里set大量的数据时,需要把过期时间错开来。但平时的的应用,这种情况比较少,一般往缓存里set数据时,都是随着用户的访问时刻不同,而自然的错开来。
加JVM的同步锁
对某个key只允许一个线程查询数据和写缓存,其他线程等待。这样在缓存过期之后,实际只有一个线程去访问数据库,其他线程都是从缓存获取。
缺点
在分布式环境,JVM的同步锁只针对本进程有效,对跨进程、跨服务器将失效。
应用分布式锁
把上面的JVM同步锁换成分布式锁即可,可以用zookeeper或redis,但这两者用起来还是有所区别,zookeeper是阻塞式的,其他线程获取不到锁时会阻塞。而redis是非阻塞,是否获取到锁都会立即把结果返回。
应用备份缓存
如果把主缓存当做一级缓存,备用缓存就是二级缓存。当从一级缓存获取不到数据时,去获取分布式锁,如果获取不到锁,则从二级缓存获取数据。
一级缓存的失效时间短,二级缓存的失效时间长。
当往一级缓存set数据时,还需要同步到二级缓存里。