reids 缓存与数据库数据不一致、缓存过期删除问题

1、缓存与数据库数据不一致

假设线程 A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取。这会带来两个问题:

  1. 线程 B 读取到了旧值;
  2. 线程 B 是在缓存缺失的情况下读取的数据库,所以,它还会把旧值写入缓存,这可能会导致其他线程从缓存中读到旧值。

解决方案:

  • 删除缓存值或更新数据库失败而导致数据不一致,可以使用重试机制确保删除或更新操作成功。(失败借助消息队列实现重试等)
  • 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删(延迟双删:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作)。
  • 借助Redisson的读写锁(进行写操作的时候才会互斥)来实现更新数据(更推荐这种方式

为什么需要读写锁?

读写锁适合于读多写少的场合,可以提高并发效率

有一些公共数据修改的机会比较少,而读的机会却是非常多的,此公共数据的操作基本都是读,如果每次操作都给此段代码加锁,太浪费时间了而且也很浪费资源,降低程序的效率。

因为读操作不会修改数据,只是做一些查询,所以在读的时候不用给此段代码加锁,可以共享的访问,只有涉及到写的时候,互斥的访问就行

/**
 * @Description: 读写锁
 * @Author: jianweil
 * @date: 2022/1/27 20:04
 */
public class ReadWriteLockTest2 {
    private static final String KEY_LOCKED = "myLock";
    private static RedissonClient redissonClient = null;

    private static void initRedissonClient() {
        // 1. Create config object
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6380").setDatabase(1);
        // 2. Create Redisson instance
        redissonClient = Redisson.create(config);
    }

    public static void read() {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED);
        readWriteLock.readLock().lock();
        try {
            System.out.println(LocalDateTime.now()+"    "+ Thread.currentThread().getName() + "获取读锁,开始执行");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(LocalDateTime.now()+"    "+ Thread.currentThread().getName() + "释放读锁");
            readWriteLock.readLock().unlock();
        }
    }

    public static void write() {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED);
        readWriteLock.writeLock().lock();
        try {
            System.out.println(LocalDateTime.now()+"    "+ Thread.currentThread().getName() + "获取写锁,开始执行");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(LocalDateTime.now()+"    "+ Thread.currentThread().getName() + "释放写锁");
            readWriteLock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        initRedissonClient();
        new Thread(() -> read(), "Thread1").start();
        new Thread(() -> read(), "Thread2").start();
        new Thread(() -> write(), "Thread3").start();
        new Thread(() -> write(), "Thread4").start();
    }
}

结果:

2022-06-07T22:48:15.295    Thread4获取写锁,开始执行
2022-06-07T22:48:16.296    Thread4释放写锁
2022-06-07T22:48:16.300    Thread2获取读锁,开始执行
2022-06-07T22:48:16.300    Thread1获取读锁,开始执行
2022-06-07T22:48:17.301    Thread2释放读锁
2022-06-07T22:48:17.301    Thread1释放读锁
2022-06-07T22:48:17.305    Thread3获取写锁,开始执行
2022-06-07T22:48:18.306    Thread3释放写锁

Thread4和Thread3是写锁,互斥,所以Thread4执行完成释放写锁,其他线程才能获取读锁或者写锁,因为Thread2和Thread1在Thread4释放写锁获得了读锁,读读是共享的,所以可以交替执行。而Thread3写锁必定在Thread4写锁之后执行。

2、缓存设置过期时间后是如何删除的

2.1、Redis对于过期键有三种清除策略

  • 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
  • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期(默认每100ms)主动淘汰一批已过期的key,这里的一批只是部分过期key,所以可能会出现部分key已经过期但还没有被清理掉的情况,导致内存并没有被释放
    • 配置文件hz参数来控制,取值范围1~500,默认是10,代表每秒运行10次
      1. 遍历所有的db
      2. 从db中设置了过期时间的key的集合中随机检查20个key
      3. 删除检查中发现的所有过期key
      4. 如果检查结果中25%以上的key已过期,则继续重复执行步骤2-3,否则继续遍历下一个db
  • 策略删除:当前已用内存超过maxmemory限定时,触发主动清理策略,redis.conf中配置的值
    在这里插入图片描述

主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:

  • 针对设置了过期时间的key做处理:

    1. volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
    2. volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
    3. volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
    4. volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。
  • 针对所有的key做处理:

    1. allkeys-random:从所有键值对中随机选择并删除数据。
    2. allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
    3. allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。
  • 不处理:

    1. noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。

2.2、LRU 算法(Least Recently Used,最近最少使用)

淘汰很久没被访问过的数据,以最近一次访问时间作为参考。

2.3、LFU 算法(Least Frequently Used,使用的频率最低)

淘汰最近一段时间被访问次数最少的数据,以次数作为参考。

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。这时使用LFU可能更好点

根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。
在这里插入图片描述

当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同步到从结点删除数据

2.4、主从模式从节点读取已过期数据问题

Redis 为了避免过多删除操作对性能产生影响,每次随机检查数据的数量并不多。如果过期数据很多,过期数据一直没有再被访问的话,这些数据就会留存在 Redis 实例中。业务应用之所以会读到过期数据,这些留存数据就是一个重要因素。

redis 3.2 之前的版本,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。在 3.2 版本后,Redis做了改进,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。

如何更有效的避免读取过期数据?
  • EXPIRE 和 PEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间;

    • 假设当前时间是 2022 年 6 月 4 日上午 9 点,主从库正在同步,主库收到了一条命令:EXPIRE testkey 60,这就表示,testkey 的过期时间就是 4 日上午 9 点 1 分,主库直接执行了这条命令。但是,主从库全量同步花费了 2 分钟才完成。等从库开始执行这条命令时,时间已经是 9点 2 分了。而 EXPIRE 命令是把 testkey 的过期时间设置为当前时间的 60s 后,也就是 9点 3 分。如果客户端在 9 点 2 分 30 秒时在从库上读取 testkey,仍然可以读到 testkey的值。但是,testkey 实际上已经过期了。
  • EXPIREAT 和 PEXPIREAT:它们会直接把数据的过期时间设置为具体的一个时间点。

    • 例如 EXPIREAT testkey 1654308000 ,可以让 testkey 在2022 年 6 月 4 日上午 10 点过期

    在这里插入图片描述
    在业务应用中使用 EXPIREAT/PEXPIREAT 命令,把数据的过期时间设置为具体的时间点,避免读到过期数据

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值