Redis 缓存不一致 - 延迟双删

概述

Redis 缓存不一致是指实际存储在数据库里的数据和缓存在Redis的数据不一致。而导致这个问题的原因是数据更新所导致的。只有数据发生了更新才有可能导致存储在两个位置(DB和Redis)的数据不一致。那么是不是只有我们在更新数据的时候,同时更新数据库和Redis就可以了呢?看似简单直接的方案往往藏了很多坑。因为这两个数据源的更新并不是原子性的,在并发情况下有诸多问题。为了简化对多数据源的管理,一般的方案都是选择更新数据库而选择让删除缓存使缓存失效。这样在获取数据的时候,会自动去数据库拉取最新的数据。
那么是不是我们只要选择更新数据,删除Redis缓存就可以实现缓存一致呢?

如何删除缓存

更新数据的时候使缓存失效,从而在需要数据的时候重新从数据库获取最新的数据,来刷新缓存达到缓存一致的目的。这个思路是对的。那么我们应该在什么时候去删除缓存呢?在更新数据库之前还是更新数据库数据之后呢?朴素的直觉告诉我们,肯定是在更新完数据库之后再来删除缓存即可。

更新后删除缓存

我们在更新数据库成功之后,再删除缓存。因为两个操作不是原子性的,所以在两个操作之间读取数据的线程依旧会短暂的读取到旧的缓存数据,然而这并不是什么太大的问题。在并发的极端情况下甚至会出现脏数据。
延迟双删

并发更新下的脏数据

当多个线程同时同时更新缓存,同时存在读取数据的线程。每一个更新的操作都可能穿插执行。在某些情况下会生成脏数据。
更新后删除缓存极限情况下的脏数据

延迟删除

生成脏数据的原因是因为在并发情况下,读取数据数据的操作发生在更新数据库数据之前,而写缓存的操作却发生在删除缓存之后。所以我们可以延迟删除缓存操作,将缓存删除的操作延迟到脏数据写缓存之后。这样就可以删除缓存脏数据。延迟删除虽然可以缓解在并发情况下对脏数据的清理。但是也极大的拉长了缓存数据的更新时间。
延迟删除

延迟双删

我们可以在更新前就删除数据,可以极大的减少缓存数据的延迟。更新前就删除数据,只要数据读取不是发生在更新数据库之前,依旧可以读取到最新的数据。
延迟双删

当出现极端情况下的脏数据时,延迟删除可以补偿对脏数据的删除。从而达到最终的数据一致性。
延迟双删

总结

综合而言,我们可以通过延迟双删来解决在并发情况导致的缓存数据不一致的问题

  1. 删除缓存
  2. 更新数据库
  3. 延迟等待一段时间
  4. 重新删除缓存

归根结底,该问题的本质其实就是并发问题。如果我们再极端假设,延迟删除依旧会发生在写数据之前(在多副本的情况下概率更大)。还是会导致数据不一致的问题。我们可以通过加分布式锁来解决问题。

在这里给自己立一个flag,2023年开始好好的打理自己的博客。争取以后可以写到简历里。

MongoDB 聚合查询

概述

Mongo 的 aggregate 类似Java的Stream。是一种流式的操作,和 Linux 的管道类似。aggregate 的参数是一个数组。数据依次通过数组中的每一个操作,传递到下一个操作。所以数组中的操作顺序对结果是有影响的。其中每一个操作的入参都是上一个操作的出参。数组中的每一个操作就类似 Java Stream 中的 Map 操作。
其中的操作可以理解为是一种DSL。$开头的对象key就是一个动作,表示需要执行的行为。而对象值里的$就是一种取值行为。
另外一点就是。对 MongoDB 的所有操作需要跳出SQL查询对我们的影响。不能以结构化查询语句的思维去考虑 MongoDB 的查询。可理解为 MongoDB 是使用一种披着JS外衣的DSL查询。它的查询更贴近JS,而不是SQL。

实操

准备数据

// 准备数据
for (let index = 1000; index < 1200; index++) {
    db.getCollection('student').insertOne({
        no: index,
        name: `STU-${index}`,
    })
}
db.getCollection('student').find();

// course
for (let index = 1; index < 6; index++) {
    db.getCollection('course').insertOne({
        no: `course-no-${index}`,
        name: `course-name-${index}`
    })
}

db.getCollection('course').find();

// selection
for (let index = 1000; index < 1200; index++) {
    for (let i = 0; i < 3; i++) {
        let no = Math.floor(Math.random() * 5) + 1;
        db.getCollection('selection').updateOne(
            {
                'student_no': index,
                'course_no': `course-no-${no}`
            },
            {
                $set: {
                    'student_no': index,
                    'course_no': `course-no-${no}`
                }
            },
            {
                upsert: true,
            }
        )
    }
}
db.getCollection('selection').find();
Java Servlet

引言

从程序说起

什么是程序?程序就是对数据的处理,然后输出结果。一个程序可以没有输入(可以理解为已经将输入包含在程序内部了),但是一定需要一个输出。这个输出可以是修改了某个设置,或者打印一个字符串。包括其他的一些音频等数据信息。
对于一个开发语言而言,语言本身只包含逻辑处理相关的,以及如何操作内存中的数据。高级语言会封装这些针对内存的数据操作,例如接受一个输入或者向屏幕输出。
例如 Java 的 CLI 程序。System.in.read() 就是输入,System.out.println()就是针对命令行的输出。

Servlet 对于 Java 而言。相当于 CGI 对 PHP。Tomcat 与 Servlet 的关系,相当于 Nginx 与 PHP-FPM。
Nginx + PHP-FPM(PHP FastCGI Process Manager)
Tomcat + Servlet

资料

Spring、Spring Boot、Spring Cloud 的区别

  • Spring 就是一个框架,支持Ioc,AOP的框架。可以简化工程类代码
  • Spring Boot 简化了 Spring 的配置,简单的说就是预配置的Spring
  • Spring Cloud 是一套 Spring微服务解决方案。提供了基于微服务的基础组件,例如服务发现,网关等