一、是什么
概念:修改数据库的同时要更新缓存,让数据库和缓存保持一致
为什么数据库和缓存会不一样呢?
二、原因
因为线程1把缓存删了,线程2没命中,直接把旧数据读出来写到缓存中,而此时还未把新数据写到数据库,导致缓存里是旧数据
先操作数据库,再删除缓存也会出现脏数据
也就是说线程1未命中查询到的数据库是旧数据,直接写入缓存了,返回数据了,而此时,线程2正把新数据写入数据库呢,线程1没读到新数据
总结一句话,就是不管先删缓存,还是先改数据库,都可能会出现把旧数据缓存到redis,出现脏数据的情况,所以我们需要在修改数据库前后删两次缓存来保证数据库和redis数据的一致性。
三、解决方案
解决方案1:延迟双删
为什么第二次删除缓存的时候要延时呢?
因为主数据库要把数据写到从数据库上需要时间,但是因为有延时所以还是会有脏数据。
解决方案2:读操作使用共享锁,写操作使用排他锁
共享锁:读不互斥,写互斥
排他锁:读写都互斥
代码示例
读操作使用共享锁
public Item getById(Integer id) {
// 获取读写锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
// 获取读锁(读写锁中的读操作部分)
RLock readLock = readWriteLock.readLock();
try {
// 上锁
readLock.lock();
System.out.println("readLock...");
// 从 Redis 缓存中尝试获取项
Item item = (Item) redisTemplate.opsForValue().get("item:" + id);
// 如果缓存中有项,则直接返回
if (item != null) {
return item;
}
// 如果缓存中没有项,创建新项
item = new Item(id, "华为手机", "华为手机", 5999.00);
// 将新项存入 Redis 缓存
redisTemplate.opsForValue().set("item:" + id, item);
// 返回新创建的项
return item;
} finally {
// 解锁
readLock.unlock();
}
}
写操作使用排他锁
public void updateById(Integer id) {
// 获取读写锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
// 获取写锁(读写锁中的写操作部分)
RLock writeLock = readWriteLock.writeLock();
try {
// 上锁
writeLock.lock();
System.out.println("writeLock...");
// 模拟更新操作
Item item = new Item(id, "华为手机", "华为手机", 5299.00);
// 模拟延迟操作,可能表示某种复杂的更新过程
try {
Thread.sleep(10000); // 延迟 10 秒
} catch (InterruptedException e) {
e.printStackTrace(); // 捕获并打印中断异常
}
// 删除 Redis 缓存中的项
redisTemplate.delete("item:" + id);
} finally {
// 解锁
writeLock.unlock();
}
}
优点:强一致性
缺点:性能较差
解决方案3:使用MQ异步通知
每次修改数据库,我们都发布消息给MQ,缓存随时监听MQ的变化,如果有新的消息,再更新缓存,在高并发下会有数据不一致的情况,但是我们可以保证最终数据的一致性。
解决方案4:使用Canal异步通知
canal记录了所有数据定义数据操作的语句,不包含查询语句
流程如上,每次修改数据库,我们都把消息发给canal,由canal通知缓存数据变化情况,再更新数据,也能保证数据最终的一致性
视频地址: