Redis双写一致性

144

什么是双写一致性?

概念:当修改了数据库的数据,也要同时更新缓存的数据,缓存和数据库的数据要保持一致

这个场景下读操作分为两种:

①缓存命中,直接返回;

②缓存未命中,去查数据库,把查询到的结果写入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数据更新缓存,