当我们使用缓存时,必定会遇到缓存一致性问题,也就是在读写请求过程中数据库和缓存中的数据不一致。
下面将分析为什么会造成不一致, 所有的代码参考末尾。
先更新数据库,后更新缓存
读操作:
1
2
3
4
5
6
7
8
9
10
11
12
| public String get(Long id) {
// 从缓存中加载
String userName = userCache.queryUserNameById(id);
if (userName == null) {
// 从数据库中加载
userName = userDB.queryUserNameById(id);
// 设置到缓存中
TestUtil.sleep(200); // 表示 gc,请求延迟
userCache.setUserNameById(id, userName);
}
return userName;
}
|
写操作:
1
2
3
4
5
6
7
| public void set(Long id, String username) {
// 更新数据库
TestUtil.sleep(100); // 表示 gc, 请求延迟
userDB.setUserNameById(id, username);
// 更新缓存
userCache.setUserNameById(id, null);
}
|
实际执行过程:
- 读操作(从缓存中读取数据,发现为空,所以查询数据库,得到 0)
- 写操作(更新数据库值为 1,删除缓存值)
- 读操作(更新缓存值为 0)
- 不一致(数据库值为 1,缓存值为 0)
从上面可以分析,更新数据库和更新缓存的顺序,无论谁先谁后都会造成数据不一致。
同时更新数据
写操作(A):
1
2
3
4
5
6
7
8
| // username = 1
public void set1(Long id, String username) {
// 更新缓存
TestUtil.sleep(100);
userCache.setUserNameById(id, username);
// 更新数据库
userDB.setUserNameById(id, username);
}
|
写操作(B):
1
2
3
4
5
6
7
8
| // username = 2
public void set2(Long id, String username) {
// 更新缓存
userCache.setUserNameById(id, username);
// 更新数据库
TestUtil.sleep(200);
userDB.setUserNameById(id, username);
}
|
实际执行过程:
- B(更新缓存值为 2)
- A(更新缓存值为 1,更新数据库值为 1)
- B(更新数据库值为 2)
- 不一致(数据库值为 2,缓存值为 1)
从上面可以分析,更新数据库和更新缓存的顺序,无论谁先谁后都会造成数据不一致。
解决方法
- 使用分布式锁来确保更新数据库和更新缓存是原子性。
代码
demo-cache-consistency-question