Redis双写一致性
什么是双写一致性?
概念:当修改了数据库的数据,也要同时更新缓存的数据,缓存和数据库的数据要保持一致
这个场景下读操作分为两种:
①缓存命中,直接返回;
②缓存未命中,去查数据库,把查询到的结果写入redis,再返回结果。
写操作:延时双删
正常来说有 删除缓存-->修改数据库 这两步不就够了吗?
如果线程之间都按设想“谦谦有礼”的话是没问题的,但是如果出现交叉运行的情况,很容易出现数据库和redis数据不一致的情况,因此再删除一次缓存,就是为了降低脏数据的出现。
那为什么要延迟一会再删除呢?
因为数据库一般是主从模式,我们需要延迟一会让主节点数据同步到从节点,因此可以延时双删可以极大程度上控制脏数据的风险,但不能完全避免,因为延时多久无法控制,而在延时这段时间内,就会出现脏数据。
那么有没有一种办法保证数据的强一致性呢?
很容易我们能想到加锁,但是弊端也很明显,那就是性能不好。
那怎么办呢?我们可以发现缓存,主要还是读的多,写的少。办法来了,可以引入读写锁。
什么是读写锁?
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作。
排他锁:独占锁也叫writeLock,加锁之后,阻塞其他线程读写操作。
看看代码如何实现:
public Item getById(Integer id) {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
// 读之前加读锁,读锁的作用就是等待该Lock key释放写锁以后再读
RLock readLock = readWriteLock.readLock();
try {
// 开锁
readLock.lock();
System.out.println("readLock...");
Item item = (Item) redisTemplate.opsForValue().get("item:" + id);
if (item != null) {
return item;
}
// 查询业务数据
item = new Item(id, "华为手机", "华为手机", 5999.00);
// 写入缓存
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);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 删除缓存
redisTemplate.delete("item:" + id);
} finally {
writeLock.unlock();
}
}
redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");之间拿到的锁得是一样的
当我们操作updateById方法里面的try代码块内容的时候,实际上getById方法里面的读操作的也会互斥。
总结:要数据强一致性,采用Redisson提供的读写锁
①共享锁:读锁readLock,加锁之后,其他线程可以共享读操作。
②排他锁:也叫独占锁writeLock,加锁之后,阻塞其他线程读写操作。
如何实现读写互斥?
其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
那有没有办法保证数据一致性,性能又好呢?
异步通知保证数据的最终一致性。
MQ实现
修改数据库数据 --> 发布消息给MQ --> 监听MQ消息 --> 更新redis
缺点在于:消息发送给MQ以后,什么时候监听到MQ消息,什么时候同步缓存,这是不确定的。
好处在于:如果MQ可靠的话,数据最终是可以保证一致性的。
总结:使用MQ中间件,更新数据之后,通知缓存删除。
Canal实现
除了可以用MQ,也可以用阿里的canal异步通知,canal是基于mysql的主从同步来实现的,当有数据进入到数据库,数据库发生了变化,就会把变化记录到binlog的二进制日志文件中
【(它记录了所有的DDL和DML语句,即数据定义语言和数据操纵语言),但不包括数据查询(select,show)语句】
然后canal就可以去监听mysql的binlog发生的数据变化,当有我们需要的表数据发生变化了,就可以在缓存服务这一块获取到变化后的数据,然后再更新到缓存。
好处:对于代码几乎是0侵入的。
坏处:数据一致会有短暂延迟,但也是在大并发情况下。
总结:使用canal中间件,不需要修改业务代码,伪装成mysql的一个从节点,canal通过读取binlog数据更新缓存,