缓存雪崩问题

  缓存雪崩一般形容的是,高并发环境中,某一个数据,因为缓存过期,读不到数据而从数据库读取,一下子大量请求堆积到了数据库上。而数据库的并发能力有限,最终导致获取不到数据库连接,读不了数据库。

模拟缓存雪崩

修改MySQL最大连接数

  缓存雪崩,最终结果并不是缓存的访问出问题,而是数据库,以MySQL为例,可以通过修改数据库的最大连接数。
修改my.inf配置文件
max_connections=5

创建模拟并发运行环境

  Spring配置数据源和JdbcTemplate,这里就不贴出来了。创建Dao和Service类,这两个类其实是很简单的,从数据库读取数据并算出总额,因为数据量比较大(10万条记录),响应的时间相对较长超过200ms。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Repository
public class OrderDao {
@Resource
private JdbcTemplate jdbcTemplate;
public long getAmount() {
String sql = "select sum(amount) from user where code = 'zhang'";
return jdbcTemplate.queryForObject(sql, Long.class);
}
}
@Service
public class OrderService {
@Resource
private OrderDao orderDao;
public long getAmount() {
return orderDao.getAmount();
}
}

  创建测试类,这个测试类是模拟多个线程同时请求orderService.getAmount()这个方法。修改count值可以设定并发数量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@RunWith(SpringRunner.class)
@ContextConfiguration(locations="classpath:spring-core.xml")
public class TestApp {
private static final Logger logger = LoggerFactory.getLogger(TestApp.class);
private int count = 3000;
private CountDownLatch countDown = new CountDownLatch(count);
@Resource
private OrderService orderService;
@Resource
private JedisPool pool;
private static final String REDIS_KEY = "amount";
@Before
public void before() {
Jedis jedis = pool.getResource();
long amount = orderService.getAmount();
jedis.set(REDIS_KEY, String.valueOf(amount));
jedis.expire(REDIS_KEY, 1);
}
@Test
public void test() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread [] threads = new Thread[count];
for (int i = 0; i < count; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
try {
logger.info("准备就绪");
countDown.await();
} catch (Exception e) {
e.printStackTrace();
}
showAmount();
}
});
threads[i].start();
countDown.countDown();
}
for (int i = 0; i < count; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
logger.info("InterruptedException",e);
}
}
}
private void showAmount() {
Jedis jedis = pool.getResource();
long amount = 0;
String str = jedis.get(REDIS_KEY);
if (StringUtils.isEmpty(str)) {
amount = orderService.getAmount();
jedis.set(REDIS_KEY, String.valueOf(amount));
jedis.expire(REDIS_KEY, 10);
logger.info("从mysql获取");
} else {
logger.info("从redis获取");
amount = Long.parseLong(str);
}
jedis.close();
logger.info("amount:" + amount);
}
}

  运行测试类后出现too many connnections的异常,说明数据库的并发请求达到瓶颈。
too many connnections
  整个执行的过程:

  1. 往redis放置一个数据,设置过期时间为1秒
  2. 启动3000个线程,同时请求一个接口,获取总额数量
  3. 获取总额时,先从redis获取,如果获取不到,则从数据库读取。
    redis的访问速度非常快,很有可能这3000个线程执行完毕,缓存中的数据都还没过期,这样就达不到效果,为了在并发访问过程中,缓存能刚好在这时间段里过期。有两种方法:增加并发数量,或者缩短缓存过期时间和并发启动的时间差。所以在test()方法执行开始,先sleep500毫秒。

缓存雪崩的原因

  在缓存过期一刹那,有大量的线程从缓存获取数据,获取不到->从数据库读取数据->放入缓存中。这个过程虽然很短暂,但是在数据还未被重新放入缓存之前,其他线程都是要走数据库获取这一步。在高并发环境中,数据库一下子堆积了大量请求,处理不过来,最终导致数据访问失败。

解决方法

设置分散的缓存过期时间

  对不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。这种情况只针对于在某个时刻往缓存里set大量的数据时,需要把过期时间错开来。但平时的的应用,这种情况比较少,一般往缓存里set数据时,都是随着用户的访问时刻不同,而自然的错开来。

加JVM的同步锁

  对某个key只允许一个线程查询数据和写缓存,其他线程等待。这样在缓存过期之后,实际只有一个线程去访问数据库,其他线程都是从缓存获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void showAmount() {
Jedis jedis = pool.getResource();
long amount = 0;
String str = jedis.get(REDIS_KEY);
if (StringUtils.isEmpty(str)) {
//加同步锁,只允许一个线程访问数据库
lock.lock();
if (jedis.get(REDIS_KEY) == null) {
logger.info("从MySQL获取");
amount = orderService.getAmount();
jedis.set(REDIS_KEY, String.valueOf(amount));
jedis.expire(REDIS_KEY, 10);
}
lock.unlock();
} else {
logger.info("从redis获取");
amount = Long.parseLong(str);
}
jedis.close();
logger.info("amount:" + amount);
}

缺点
在分布式环境,JVM的同步锁只针对本进程有效,对跨进程、跨服务器将失效。

应用分布式锁

  把上面的JVM同步锁换成分布式锁即可,可以用zookeeper或redis,但这两者用起来还是有所区别,zookeeper是阻塞式的,其他线程获取不到锁时会阻塞。而redis是非阻塞,是否获取到锁都会立即把结果返回。

应用备份缓存

  如果把主缓存当做一级缓存,备用缓存就是二级缓存。当从一级缓存获取不到数据时,去获取分布式锁,如果获取不到锁,则从二级缓存获取数据。
  一级缓存的失效时间短,二级缓存的失效时间长。
  当往一级缓存set数据时,还需要同步到二级缓存里。

参考资料