引言

Redis是最流行的缓存数据库。本文介绍Redis的缓存设计模式。

一、缓存模式

1.1 Cache-Aside(旁路缓存)

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
public User getUser(Long id) {
// 1. 查缓存
User user = redis.get("user:" + id);
if (user != null) {
return user;
}

// 2. 查数据库
user = userDao.selectById(id);

// 3. 写缓存
if (user != null) {
redis.setex("user:" + id, 3600, user);
}

return user;
}

public void updateUser(User user) {
// 1. 更新数据库
userDao.updateById(user);

// 2. 删除缓存
redis.del("user:" + user.getId());
}

1.2 Read-Through

1
2
3
4
5
// 缓存层自动加载数据
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userDao.selectById(id);
}

1.3 Write-Through

1
2
3
4
5
6
// 更新时同步写入缓存和数据库
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
userDao.updateById(user);
return user;
}

二、缓存问题

2.1 缓存穿透

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 问题:查询不存在的数据
// 解决:布隆过滤器或缓存空值
public User getUser(Long id) {
// 布隆过滤器检查
if (!bloomFilter.mightContain(id)) {
return null;
}

User user = redis.get("user:" + id);
if (user != null) {
return user;
}

user = userDao.selectById(id);

// 缓存空值
if (user == null) {
redis.setex("user:" + id, 60, "null");
return null;
}

redis.setex("user:" + id, 3600, user);
return user;
}

2.2 缓存雪崩

1
2
3
4
5
6
7
8
// 问题:大量缓存同时失效
// 解决:过期时间加随机值
int expire = 3600 + new Random().nextInt(300);
redis.setex(key, expire, value);

// 永不过期(逻辑过期)
data.expireTime = System.currentTimeMillis() + 3600000;
redis.set(key, data);

2.3 缓存击穿

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
// 问题:热点数据过期瞬间大量请求
// 解决:互斥锁
public User getUser(Long id) {
User user = redis.get("user:" + id);
if (user != null) {
return user;
}

// 获取互斥锁
String lockKey = "lock:user:" + id;
if (redis.setnx(lockKey, "1", 10)) {
try {
user = userDao.selectById(id);
redis.setex("user:" + id, 3600, user);
} finally {
redis.del(lockKey);
}
} else {
// 等待后重试
Thread.sleep(50);
return getUser(id);
}

return user;
}

三、缓存更新策略

1
2
3
4
5
6
7
8
9
// 1. 超时失效
redis.setex(key, 3600, value);

// 2. 主动更新
redis.set(key, newValue);

// 3. 先更新数据库,再删除缓存
userDao.updateById(user);
redis.del("user:" + user.getId());

四、缓存预热

1
2
3
4
5
6
7
@PostConstruct
public void warmUp() {
List<User> hotUsers = userDao.selectHotUsers();
for (User user : hotUsers) {
redis.setex("user:" + user.getId(), 3600, user);
}
}

总结

Redis缓存核心:

  1. 缓存模式:Cache-Aside最常用
  2. 三大问题:穿透、雪崩、击穿
  3. 更新策略:超时、主动更新
  4. 缓存预热:提前加载热点数据

参考资料