文章目录
MySql 缓存
本章主要介绍缓存的一些场景,如何解决MySql缓存双写一致性的问题。
1. 常见的缓存问题
1.1 缓存穿透
-
什么是缓存穿透?
一般的缓存系统都是按照key-value的形式去缓存查询的,如果不存在对应的value,就会去对应的系统查(比如MySql,Oracle,ES)。如果key对应的value是一定不存在的,并且对该key的请求量非常大,就会对后端系统造成很大的压力。也就是说,对不存在的key的value,进行高并发访问,导致数据库的压力瞬间增大,这种场景叫做缓存穿透。
-
如何避免,解决?
- 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Map中,查询时通过该Map过滤(该方法不通用)。
// 伪代码
final Map<String, String> keys = new HashMap<>();
public boolean existKey(String key) {
// true,进行查询流程
if(keys.containsKey(key)) {
return true;
} else {
// 不存在对应key的value,直接返回空数据给客户端
return false;
}
}
- 对查询结果为空的情况也进行缓存,对空的数据进行判断,缓存时间设置短一点;对该key对应的数据insert了之后清理缓存,防止存在key对应的value,查询结果为空(此方法通用)。
1.2 缓存雪崩
- 缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。 - 如何解决
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
**推荐:**方式一与方式二结合使用。
1.3 缓存击穿
-
什么是缓存击穿?
对于一些设置了过期时间的key,如果这些key可能会在某个时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 -
如何解决?
使用锁,建议redis分布式锁,兼容程序在后续的分布式部署
// 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
// 伪代码
if(redis.sexnx()==1) {
// 1. 先查缓存
Object obj = getCashByKey(key);
if(obj == null) {
// 2. 缓存无数据,查询数据
obj = selectDb(key);
// 3. 加入缓存
setCash(key, obj);
}
return obj;
}
1.3 缓存双写一致性
一般来说,在读取缓存方面,我们都是先读取缓存,再读取数据库的。但是,在更新缓存方面,我们是需要先更新缓存,再更新数据库?还是先更新数据库,再更新缓存?
1.3.1 先更新数据量再更新缓存(不建议使用)
- 场景:线程A和线程B同时对同一数据进行操作
- 线程A修改了数据量
- 线程B修改了数据库
- 线程B更新的缓存
- 线程A更新了缓存
-
现象
数据库的数据的B线程,缓存中的数据是A线程的。 -
问题
- 脏读。
- 浪费性能。
1.3.2 先删除缓存再更新数据库(不推荐)
- 场景:线程A更新数据,线程B读取数据
- 线程A删除缓存,更新数据,此刻未提交事务(即未写操作)
- 线程B读取数据,缓存无数据,将数据写入缓存
- 线程A提交事务(写入数据库)
-
现象
数据库是新数据,线程B读取旧数据,缓存中是旧数据 -
问题
- 脏读。
1.3.3 先更新数据库再删除缓存(推荐)
- 场景:线程A更新,线程B读,线程C读。
- 线程A进行更新操作,此时线程A还未提交事务(即未写操作)
- 线程B进行读操作,此时缓存无数据
- 线程B去数据库查询,得到旧值,并将旧值写入缓存
- 线程A提交事务,新数据写入数据库,删除缓存
- 线程C读操作,此时缓存无数据,查询数据库得到新值,并将其写入缓存
- 现象
数据库是新的数据,线程B是旧的数据,线程C是新的数据,缓存中是新的数据。无论几个线程对同一数据读写操作,最终缓存的数据要么是空的,要么是新数据。
// 伪代码
try {
updateDate(data);
} catch(Exception e) {
// 回滚事务
db.rollback();
} finally {
// 无论更新成功或者是失败,都删除缓存,等待下一次查询在存储
delCash();
}
2 结论
-
深刻认识缓存穿透,缓存雪崩,缓存击穿,三者的区别、场景。
缓存穿透:对不存在key的value,在某一时间点被高并发访问,数据库压力瞬间增大;不存在指的是value为空,即数据库查询出来的结果也是空。
缓存雪崩: 大量缓存集中在某一个时间段失效,这样在失效的时候,对这些失效的key高并发访问会给后端系统(比如DB)带来很大压力。
缓存击穿:与缓存雪崩的区别在于,前者针对的是一批key,后者仅针对一个key。 -
缓存双写一致性解决方案:先更新数据库,finally删除缓存,等待下次查询重新写入缓存。
-
现象:目前的大部分企业不再使用MySql内置的缓存,更多的偏向于使用缓存数据库,例如redis。
有误区,欢迎大牛指正交流。