家具网站建设便宜/泉州seo代理计费

家具网站建设便宜,泉州seo代理计费,个人做同城网站赚钱吗,网站做支付端口的费用目录 一. 业务数据查询,更新顺序简要分析 二. 更新数据库、查询数据库、更新缓存、查询缓存耗时对比 2.1 更新数据库(最慢) 2.2 查询数据库(较慢) 2.3 更新缓存(次快) 2.4 查询缓存&#…

目录

一. 业务数据查询,更新顺序简要分析

二. 更新数据库、查询数据库、更新缓存、查询缓存耗时对比

2.1 更新数据库(最慢)

2.2 查询数据库(较慢)

2.3 更新缓存(次快)

2.4 查询缓存(最快)

三. 数据一致性更新策略举例说明

3.1 先更新数据库,再更新缓存

3.2 先更新缓存,再更新数据库

3.3 先删除缓存,再更新数据库

3.4 先更新数据库,再删除缓存

3.5 方案对比与选择

3.5.1 最好先操作数据库,后操作缓存

3.5.2 最好删除缓存,而不是更新缓存

3.5.3 具体场景具体分析

四. 低频修改数据场景 的 推荐解决方案

五. 高频修改数据场景 的 推荐解决方案

5.1. canal 入门

5.1.1 canal 简介

5.1.2 canal 下载和配置修改

5.1.3 canal 运行和确认

5.2 MySQL 配置

5.2.1 windows 环境配置修改

5.2.2 windows 环境配置生效验证

5.2.3 创建 canal 所需要的数据库权限

5.3 Binlog 监听 + 消息队列 流程图简要分析

5.4 代码实例

5.4.1. 业务服务(更新数据库)

5.4.2. Canal 客户端(监听 Binlog)

5.4.3. 缓存同步服务(删除缓存 + 失败入队)

5.4.4. 消息队列消费者(重试删除)

六. 资金账户类敏感数据 的 推荐解决方案

七. 面试题合集


Redis,MySQL 双写一致性主要是指在使用缓存和数据库的同时存储数据时,如果在高并发的场景下,二者可能存在数据不一致的情况,因此希望尽量保证 Redis 中的数据和 MySQL 中的数据尽可能保持一致。

一. 业务数据查询,更新顺序简要分析

如下图所示,最左侧是我们的 Java 程序,最右侧是数据库MySQL,中间这一层就是缓存 Redis。

在实际业务数据查询过程中,用户访问网站数据,通常会发送查询请求,通常分为以下三步。

情况一:先查询 Redis,如果 Redis 有数据,直接返回;

情况二:先查询 Redis,但是 Redis 无数据,MySQL 有数据,再去查询MySQL,然后返回数据,同时将数据回写到 Redis 以便于下次查询;

情况三:先查询 Redis,但是 Redis 无数据、再去查询 MySQL,但是MySQL也没有数据,返回空。

查询数据没什么影响,关键在于更新数据操作,如果要更新数据库,那么缓存也要更新。

这里就会有一个问题?先动缓存还是先动数据库?缓存时更新缓存较好,还是删除缓存较好?

由此而来,就引申出了MySQL,redis 更新策略的四种情况。

情况一:先更新数据库,再更新缓存

情况二:先更新缓存,再更新数据库

情况三:先删除缓存,再更新数据库

情况四:先更新数据库,再删除缓存

二. 更新数据库、查询数据库、更新缓存、查询缓存耗时对比

2.1 更新数据库(最慢)
  • 操作逻辑:写入磁盘(如 MySQL 的 UPDATE),需保证 ACID 特性。

  • 耗时范围毫秒级到秒级(简单更新约 10~100ms,复杂事务或高并发下更慢)。

  • 关键瓶颈

    • 事务提交:需写事务日志(如 Redo Log)、刷盘(fsync)和同步副本(主从架构)。

    • 锁开销:行锁、间隙锁等可能阻塞其他操作,尤其在并发场景。

    • 索引维护:更新可能触发 B+ 树分裂、索引重建等额外开销。

2.2 查询数据库(较慢)
  • 操作逻辑:从磁盘(如 MySQL)读取数据,可能涉及索引扫描、锁等待或复杂查询。

  • 耗时范围毫秒级(简单主键查询约 1~10ms,复杂查询可达 100ms+)。

  • 关键瓶颈

    • 磁盘 I/O:随机读性能远低于内存(机械硬盘约 1ms/次,SSD 约 0.1ms/次)。

    • 锁竞争:若查询涉及行锁或表锁,可能因事务冲突增加等待时间。

    • 网络延迟:应用层与数据库分离时,需叠加网络 RTT(通常 0.1~1ms)。

2.3 更新缓存(次快)
  • 操作逻辑:写入内存(如 Redis 的 SET 或 DEL)。

  • 耗时范围微秒级(Redis 单次写操作约 0.1~0.5ms)。

  • 关键差异

    • 写操作可能触发内存分配、序列化或淘汰策略(如 LRU),略慢于读操作。

    • 若开启持久化(如 AOF),写入需追加日志,但通常异步执行,不影响主线程。

2.4 查询缓存(最快)
  • 操作逻辑:直接从内存(如 Redis)读取数据,无磁盘 I/O 或复杂计算。

  • 耗时范围微秒级(Redis 单次读操作约 0.1ms 内)。

  • 关键优势

    • 内存操作,无物理寻址延迟。

    • 单线程模型(如 Redis)避免锁竞争,响应稳定。

不难看出,操作缓存的耗时在操作数据库耗时前几乎约等于没有,所以下面我们重点对比线程之间对于数据库操作的耗时即可,了解了这一点,我们再往下来探究一致性更新策略的对比。
 

三. 数据一致性更新策略举例说明

常见的四种更新策略,为 先更新数据库,再更新缓存、先更新缓存,再更新数据库、先删除缓存,再更新数据库、先更新数据库,再删除缓存

假设A,B两个线程同时发起调用。线程A固定为写操作,线程B可能是读,也可能是写操作。

数据库商品表 product 现在商品数量 number 为100;

3.1 先更新数据库,再更新缓存

正常逻辑:

(1)A update mysql = 90

(2)A update redis = 90

(3)B update mysql = 80

(4)B update redis = 80

多线程情况下,A,B会有快有慢,可能出现如下

异常逻辑:

(1)A update mysql = 90

(2)B update mysql = 80

(3)B update redis = 80

(4)A update redis = 90

A 更新数据库,在准备写入缓存时,B先更新了数据库,并将数据写入缓存,然后A又完成了数据库的更新。最终结果导致MySQL值为80,Redis 值为90,数据不一致。

造成这种情况需要的条件(不考虑网络延迟):

如果线程B为写操作,则需要线程A写入缓存的耗时(0.1~0.5ms) > 线程B更新数据(10~100ms)+更新缓存的耗时(0.1~0.5ms);

如果线程B为读操作,则需要线程A写入缓存的耗时要(0.1~0.5ms) > 线程B查询缓存的耗时(0.1ms 内);

考虑实际业务中,读操作往往比写操作多;总的来说,先更新数据库,再更新缓存导致数据不一致这种情况大概率会发生。

3.2 先更新缓存,再更新数据库

正常逻辑:

(1)A update redis = 90

(2)A update mysql = 90

(3)B update redis = 80

(4)B update mysql = 80

异常逻辑:

(1)A update redis = 90

(2)B update redis = 80

(3)B update mysql = 80

(4)A update mysql = 90

A 更新缓存,然后更新数据库,但是在更新数据库期间,B先来更新了缓存和数据库,然后A有更新成功了数据库。最终结果导致MySQL值为 90,Redis 值为80,数据不一致。

造成这种情况需要的条件:

若线程B为写操作:则需要线程A更新数据库耗时(10~100ms) > 线程B更新数据库耗时(10~100ms)+更新缓存耗时(0.1~0.5ms);

若线程B为读操作:则需要线程A更新数据库耗时(10~100ms) > 线程B查询缓存耗时(0.1ms 内);

考虑实际业务中,读操作往往比写操作多;总的来说,先更新缓存,再更新数据库导致数据不一致这种情况大概率会发生

3.3 先删除缓存,再更新数据库

多线程:举例A,B两个线程同时操作可能出现的问题 

(1)A delete redis 100

(2)B get number from redis,值为空;B get 100 from mysql

(3)B set 100 redis

(4)A update mysql 80

A线程先删除 redis 的数据,然后再去更新数据库进行写操作;

但是由于A的写操作慢或网络延迟,导致还未写成功,B线程来读数据,发现缓存未命中,又去数据库读数据;B在读取到就数据之后返回,并将旧数据 number 重新写入 缓存 redis。B操作做完一切之后,A线程完成了写操作,此时 mysql 的新数据80与 redis 的旧数据100不一致,数据不一致。

我们来分析一下这种情况出现的条件

若线程B为写请求:需要线程A更新数据库耗时(10~100ms) > 需要线程B更新数据库耗时(10~100ms)

若线程B为读请求:需要线程A更新数据库耗时(10~100ms) > 需要线程B查询数据库耗时(1~10ms) + 线程B写入缓存耗时(0.1~0.5ms)

考虑实际业务中,读操作往往比写操作多;总的来说,先删除缓存,再更新数据库导致数据不一致这种情况大概率会发生

3.4 先更新数据库,再删除缓存

(1)A update mysql 80

(2)B get 100 from redis

(3)A delete redis

举例:A线程先去更新数据库,100变为80,缓存先不动;

然后A再去删除缓存,但是B来了,读取到了缓存中的100,直接返回,

A 完成了删除缓存的操作。 

不难看出,这种情况,在A线程成功删除缓存之前,也会造成短时间内的脏数据。

我们来分析一下这种情况出现的条件

若线程B为写请求:需要线程A更新缓存耗时(0.1~0.5ms) > 需要线程B更新数据库耗时(10~100ms)

若线程B为读请求:需要线程A删除缓存耗时(0.1~0.5ms) > 需要线程B查询缓存耗时(0.1ms 内)

考虑实际业务中,读操作往往比写操作多;总的来说,先更新数据库,再删除缓存导致数据不一致这种情况大概率会发生

3.5 方案对比与选择

从上面的四种情况并不难看出,不管我们选择哪一种,缓存数据不一致的情况大概率都会发生。那么我们到底应该选择哪一种策略呢?

3.5.1 最好先操作数据库,后操作缓存

其实就单纯数据库支持事务这一条而言,我们就应该先操作数据库,因为如果数据库更新失败,可以进行事务回滚,或者程序重试。此时我们还尚未操作缓存,不管是更新缓存还是删除缓存,都还未进行,不会对后续其它读写线程造成影响,但如果我们先操作缓存,一旦数据库更新失败,就会导致后续其他线程进行缓存重建,浪费时间和性能,做无用功。

下面是先操作数据库后操作缓存的几个优点。

(1)降低脏数据风险

  • 若先删除缓存再更新数据库,在数据库更新完成前,若有并发请求查询数据,会因为缓存缺失读取数据库的旧值并重新写入缓存,导致缓存中保留旧数据。
  • 若先更新数据库再删除缓存,即使缓存删除失败,缓存中的旧数据也只会短暂存在(下次查询触发缓存重建会自动替换为新值),并且数据库已更新为最新数据,最终一致性可控。

(2)减少不一致的窗口时间

  • 假设更新数据库为10ms,删除缓存耗时2ms;
  • 若先删除缓存再更新数据库,数据不一致窗口期为10ms(数据库更新期间);
  • 但若是先更新数据库再删除缓存,数据不一致窗口期仅为2ms(删除缓存期间); 

(3)异常处理的容错性

  • 数据库操作通常支持事务,若更新数据库失败可以回滚,此时还未删除缓存不会引入错误。
  • 可如果先删除缓存,但数据库更新失败,此时缓存已丢失,后续的请求会穿透到数据库,并且数据库未更新成功还会导致缓存重建(额外处理),相对来说耗时间。

(4)避免高并发下的双写覆盖

  • 在高并发场景下,若A线程先删除缓存,B线程在A线程更新数据库前查询旧值并写入缓存,可能导致缓存与数据库长期不一致。
  • 但如果先更新数据库后删除缓存,即使线程B先读取到旧值,旧缓存值也会在线程A更新数据库操作完成后被清除,顶多造成短时间内数据不一致,一旦后续又有新请求,就会触发缓存重建将新数据写入缓存。

3.5.2 最好删除缓存,而不是更新缓存

其实我们也可以用懒加载这一层面来理解这个问题,更新数据库是要比删除数据库更耗费性能的,并且更新的数据不一定会马上被访问,既然如此,不如不做,等待其它读操作在需要的时候再来进行缓存重建即可,这样既tighao性能,还提高了程序的运行效率。

下面是删除缓存相对于更新缓存的优点。

(1)避免并发写入导致脏数据

  • 更新缓存:若线程A更新数据库后未完成更新缓存,由于时序或网络延迟,线程B先完成了更新数据库和缓存,线程A再写入缓存时,会导致缓存仍是旧值(期望是B修改后的值)
  • 删除缓存:无论更新顺序如何,删除缓存强制要求下一次查询加载数据库的最新值,天然规避了上面线程顺序带来的脏数据影响。

(2)降低计算资源的浪费

  • 更新缓存:每次更新数据库都需要重新计算并写入缓存,可能浪费资源(例如数据被频繁被更新但很少被读取)
  • 删除缓存:仅在数据集是被查询时重建缓存,按需使用资源。

(3)防止部分更新导致的数据不一致

  • 更新缓存:在遇到复杂缓存的数据结构时,例如Hash,List,若只更新部分字段,可能因为代码的逻辑错误或网络终端导致缓存数据与数据库不一致。
  • 删除缓存:直接删除整个键,下次查询数据时重建完整数据,避免部分更新风险。

(4)简化异常处理

  • 更新缓存:若缓存更新失败,需回滚数据库事务或重试缓存写入,增加系统复杂性。
  • 删除缓存:若缓存删除失败,可通过异步补偿机制(如消息队列),即使未删除成功,旧数据也仅短暂存在。

3.5.3 具体场景具体分析

没有哪个方案是最好的,实际开发过程中都是需要根据项目的实际情况来进行选择的。

对于数据的操作,无非就是读和写。读操作暂不关心,高频写和低频写使用的方案一般是略有变差的,如下表格所示。

场景推荐方案一致性级别实现复杂度
低频修改数据缓存过期 + 延迟双删最终一致性
高频修改数据Binlog 监听 + 消息队列最终一致性
资金账户类敏感数据

数据库事务 + 同步更新

(最好使用锁)

强一致性

下面我们就对这三类场景分别作出分析,并给出一个简要的代码逻辑。

四. 低频修改数据场景 的 推荐解决方案

场景推荐方案一致性级别实现复杂度
低频修改数据缓存过期 + 延迟双删最终一致性

如下示例代码:

// Redis 产品数量固定字符串 Key 前缀,可有可不有,但标准项目基本都会有 key 前缀,方便管理
public static final String PRODUCT_NUMBER = "PRODUCT:NUMBER:";
// 创建一个固定大小为 4 的线程池
private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(4);
@Transactional
public void updateProductNumber(Long number,String productName){// 拼接查询 keyString key = PRODUCT_NUMBER + productName;try{// (A1) 线程A首次删除缓存stringRedisTemplate.delete(key);// 这里要注意,下方查询缓存重建缓存的逻辑,可能是其他线程(线程B)在线程A更新数据库之前就已经完成了// (B1) 线程B查询数据库String numberStr = stringRedisTemplate.opsForValue().get(key);// (B2):线程B判断缓存值是否为空if (numberStr == null){// (B3) B线程查询数据库Long productnumber = orderMapper.selectProductNumber(productName);numberStr = String.valueOf(productnumber);// (B4) B线程写入缓存,并给缓存一个过期时间,这里为30分钟,如果为热点数据,建议时间更短一些,比如1分钟,3分钟,5分钟等,根据业务需要调整即可if (productnumber != null){stringRedisTemplate.opsForValue().set(key,numberStr,30,TimeUnit.MINUTES);}// (B5) 因为线程B为读线程,缓存重建后返回数据return ...;}// (A2) 线程A更新数据库orderMapper.updateProductNumber(number,productName);/* (A3) 线程A 提交事务后开辟新线程异步延迟进行缓存第二次删除(关键!)* A再次删除缓存,就像上面线程B那样,因为是查询,比线程A快,还将缓存重建,* 所以这里进行二次删除,将线程B写入到缓存的脏数据删除掉。  * */asyncExecutor.execute(() -> {try {// 这里的线程是异步的,所以不会阻塞主线程的执行,但是这样会增加开销,// 此外,延时时间需根据业务情况测试,我随便写的,这里随便写为 500ms,需根据业务情况调整。Thread.sleep(500);// (A4) 线程A第二次删除缓存,如果后续有其它读请求,就会像上面B线程一样将缓存重建stringRedisTemplate.delete(key); } catch (InterruptedException e) {Thread.currentThread().interrupt();}});// (A5) 线程A完成操作,返回数据return ...;}catch (Exception e){throw new RuntimeException("程序错误",e);}
}

给缓存设计过期时间,定期清理缓存并回写,是保证数据最终一致性的解决方案

所有的写操作都要以数据库为准,对缓存操作只是尽最大努力即可。如果数据库写成功,缓存更新失败,那么只要达到过期时间,则后面的读请求自然会从数据库读取最新的值然后回写到缓存,达到已执行。总而言之,要以数据库(MySQL)写入库的数据为准。

此种方案比较适合数据修改频率较低的情况,当然了,每个项目都有对应的特点,结合项目业务特色选择相应的解决方案即可。

五. 高频修改数据场景 的 推荐解决方案

场景推荐方案一致性级别实现复杂度
高频修改数据Binlog 监听 + 消息队列最终一致性
5.1. canal 入门
5.1.1 canal 简介

canal 是阿里巴巴旗下的一款开源项目,基于数据库增量日志解析,提供增量数据订阅&消费主要用途是基于 MySQL 数据库增量日志解析,目前主要支持MySQL。说白了是一个新的技术,第三方中间件,需要额外花时间掌握学习。有兴趣的小伙伴可以查阅下面这边文章,写的非常好!

【Canal】从原理、配置出发,从0到1完成Canal搭建-CSDN博客

canal 的工作原理类似于将自己伪装成 MySQL(主机) 的一条从机(slave),然后只要主机数据发生变化,就会同步MySQL主机的数据。想了解主从复制的可以看博主的另一篇文章:

浅谈 MySQL 主从复制,优点?原理?_mysql主从优势-CSDN博客

5.1.2 canal 下载和配置修改

canal 下载:如下图所示,点击下载接口

canal.deployer-1.1.6.tar.gz

下载完毕后,解压得到如下文件,进入 conf——>example——>instance.properties,修改instance.properties 文件。

将 address 改为自己的MySQL地址,下方两个改为自己的数据库用户名和密码,其它不用动。

5.1.3 canal 运行和确认

OK,保存文件,就改完了,然后 进入 bin 目录,双击运行 startup.bat 脚本运行即可。

然后会出现黑色窗口,我们再单独开一个 cmd 窗口,输入

netstat -ano | findstr ":11111"

出现如下就表示运行成功了!

5.2 MySQL 配置
5.2.1 windows 环境配置修改

配置文件路径
通常位于MySQL安装目录下,例如:
C:\Program Files\MySQL\MySQL Server 8.0\my.ini

直接使用文本编辑器打开即可,必须修改的配置项如下

[mysqld]
# 启用Binlog,指定日志文件名前缀
log-bin=mysql-bin# 设置Binlog格式为ROW(Canal依赖此格式)
binlog_format=ROW# 设置唯一的服务器ID(需确保与其他MySQL实例不冲突)
server-id=1# 可选:设置Binlog保留天数(默认不删除)
expire_logs_days=7
5.2.2 windows 环境配置生效验证

更改完毕配置文件后,切记最好重启MySQL服务。

然后进入 navicat ,运行如下命令确认是否配置成功。

sql
复制
-- 检查Binlog是否启用
SHOW VARIABLES LIKE 'log_bin';-- 检查Binlog格式是否为ROW
SHOW VARIABLES LIKE 'binlog_format';-- 检查Server ID
SHOW VARIABLES LIKE 'server_id';-- 检查Binlog保留天数
SHOW VARIABLES LIKE 'expire_logs_days';

5.2.3 创建 canal 所需要的数据库权限

这里记得换成自己的数据库密码!!!

-- 创建用户(需替换 YOUR_PASSWORD)
CREATE USER 'canal'@'%' IDENTIFIED WITH 'mysql_native_password' BY 'YOUR_PASSWORD';-- 授予复制权限
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';-- 授予 Binlog 访问权限(MySQL 8.0 必须)
GRANT SELECT, RELOAD, SHOW DATABASES, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'canal'@'%';-- 刷新权限
FLUSH PRIVILEGES;

5.3 Binlog 监听 + 消息队列 流程图简要分析

流程关键点总结

步骤核心目标技术实现
1-2更新数据库业务代码直接操作数据库
3-4解析 BinlogCanal 客户端监听并提取 Key
5-6首次删除缓存独立服务 + 异常降级到消息队列
7-8重试保证最终一致性消息队列异步消费

5.4 代码实例

在如下代码中,2,3,4步只需要在项目初期配置好即可,后续就不需要怎么做修改了,顶多在 canal 客户端添加监听的数据库表,此时数据同步代码基本已经与业务代码完全解耦合了。

后续我们只需要关注业务层面即可,无需分心关注数据一致性的问题。

5.4.1. 业务服务(更新数据库)
// ProductService.java
public class ProductService {@Autowiredprivate ProductMapper productMapper;// 步骤1-2:更新数据库,触发 Binlog 生成public void updateProductNumber(Long productId, Long productNumber) {// 直接操作数据库productMapper.updateProductNumber(productId, productNumber);}
}
5.4.2. Canal 客户端(监听 Binlog)

Canal 这里的配置,目前只监听了 test 数据库下的 product 表,也可以监听多张表,或者整个库,或者跨库监听都是可以的。

一般情况下,都是只监听核心业务表(高频操作表),这样不会有冗余数据,并且只监听几张表,资源占用较低。

场景示例说明
精确匹配单表test.product只订阅 test 库的 product 表
多表逗号分隔test.user,test.product订阅两个表
正则表达式匹配test\\..*订阅test库所有表
跨库匹配db1\\..*,db2.order_.*订阅db1所有表和db2的order前缀表

通常情况下,canal 还需要在 properties 或 yml 文件中进行配置。 

# Canal 连接 MySQL 的配置
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=YOUR_PASSWORD
canal.instance.connectionCharset=UTF-8
// CanalClient.java
public class CanalClient {public static void main(String[] args) {// 连接 Canal 服务端CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1", 11111), "example", "", "");connector.connect();connector.subscribe("test.product"); // 订阅 product 表的 Binlogwhile (true) {Message message = connector.getWithoutAck(100); // 拉取 Binlogfor (CanalEntry.Entry entry : message.getEntries()) {if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {// 步骤3-4:解析 Binlog,提取 KeyRowChange rowChange = RowChange.parseFrom(entry.getStoreValue());for (RowData rowData : rowChange.getRowDatasList()) {Long userId = rowData.getAfterColumnsList().get(0).getValue();String key = "user:" + userId;// 调用缓存同步服务CacheSyncService.process(key);}}}connector.ack(message.getId()); // 确认消费}}
}
5.4.3. 缓存同步服务(删除缓存 + 失败入队)
// CacheSyncService.java
public class CacheSyncService {private static RedisClient redis = new RedisClient("redis://localhost:6379");private static MessageQueue mq = new KafkaMessageQueue("kafka:9092");// 步骤5-6:尝试删除缓存,失败则发送到消息队列public static void process(String key) {try {boolean success = redis.delete(key);if (!success) {mq.send("cache_retry_queue", key); // 发送到重试队列}} catch (Exception e) {mq.send("cache_retry_queue", key); // 异常时也发送}}
}
5.4.4. 消息队列消费者(重试删除)
// MQConsumer.java
public class MQConsumer {public static void main(String[] args) {MessageQueue mq = new KafkaMessageQueue("kafka:9092");RedisClient redis = new RedisClient("redis://localhost:6379");// 步骤7-8:订阅队列并重试删除mq.subscribe("cache_retry_queue", message -> {String key = (String) message;try {boolean success = redis.delete(key);if (!success) {System.err.println("重试删除失败: " + key);// 可添加重试次数限制(例如最多重试3次)}} catch (Exception e) {mq.send("cache_retry_queue", key); // 再次入队}});}
}

六. 资金账户类敏感数据 的 推荐解决方案

场景推荐方案一致性级别实现复杂度
资金账户类敏感数据

数据库事务 + 同步更新

(最好使用锁)

强一致性

示例代码如下:

没啥可说的,就是使用了分布式锁,强制线程串行化执行,基本不存在并发导致数据不一致的情况发生。

private final AccountMapper accountMapper;
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;@Transactional(rollbackFor = Exception.class)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {RLock lock = redissonClient.getLock("account_lock:" + fromId + ":" + toId);try {if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {// MyBatis查询Account fromAccount = accountMapper.selectById(fromId);Account toAccount = accountMapper.selectById(toId);// 余额计算fromAccount.setBalance(fromAccount.getBalance().subtract(amount));toAccount.setBalance(toAccount.getBalance().add(amount));// MyBatis更新accountMapper.updateBalance(fromAccount);accountMapper.updateBalance(toAccount);// 使用StringRedisTemplate存储JSONObjectMapper objectMapper = new ObjectMapper();stringRedisTemplate.opsForValue().set("account:" + fromId,objectMapper.writeValueAsString(fromAccount));stringRedisTemplate.opsForValue().set("account:" + toId,objectMapper.writeValueAsString(toAccount));}} finally {lock.unlock();}
}

七. 面试题合集

问题一:有这样一种情况,微服务查询 Redis 无数据,MySQL 有数据,为保证数据双写一致性再回写到 Redis 时,需要注意什么?双检加锁策略了解过吗?

双检加锁其实和延时双删思路是一样的,简单来说。在高并发的情况下,如果线程A查询缓存,无数据,然后会查询数据库,发现有数据然后将数据回写到缓存,但在查询数据库期间,可能已经有其他线程(线程B)先一步完成了查询数据库并回写了缓存,此时A再回写缓存已经无意义了。所以在线程A查询到数据准备回写缓存之前,可以再进行一次判断,查看当前缓存中是否依旧为空,如果为空说明缓存还未被重建,则再去回写缓存。

示例代码如下:

public static final String USER_PREFIX = "USER:";
public User findUserById(Long id){// 拼接Redis的key,用户对象,缓存用户字符串对象;String key = USER_PREFIX + id;User user = null;String userStr = null;try {// 1. 从Redis中查询用户信息:若结果不为空,转化后直接返回//                        若结果为空,再去查询数据库userStr = stringRedisTemplate.opsForValue().get(key);if (userStr != null) {return JSONUtil.toBean(userStr, User.class);}else {/** 2. 拿到锁之后,再次查询缓存确保缓存无数据,双重检查,俗称双检* 之做所以这样做,是因为在高并发情况下,可能会存在多个线程同时进入,导致缓存已经重建,从而导致数据库被查询多次* 会对数据库服务器造成压力**/userStr = stringRedisTemplate.opsForValue().get(key);// 3. 判断是否为空,如果不为空,说明缓存已被其他线程重建,直接返回数据//                如果为空,进行缓存重建,从数据库中查询数据,然后重建缓存if (userStr != null) {return JSONUtil.toBean(userStr, User.class);} else {// 4. 从数据库中查询用户信息user = userMapper.selectById(id);if (user == null){// 如果是一个热点key,应该回写到redis里一个空值,避免缓存穿透,时间根据业务需求自己定,我随便写的stringRedisTemplate.opsForValue().set(key, "", 1, TimeUnit.MINUTES);return null;}// 5. 将对象转为jsonuserStr = JSONUtil.toJsonStr(user);// 6. 写入RedisstringRedisTemplate.opsForValue().set(key, userStr, 3, TimeUnit.DAYS);}}} catch (Exception e) {logger.error("程序发生错误失败", e);}return user;
}

下面这四个问题,答案全都已经在文章中了,就留给小伙伴们自行解答啦! 

问题二:只要使用缓存,就可能涉及到 Redis 缓存与数据库双存储双写,只要有双写,就一定有数据一致性问题,如何解决数据一致性问题?

问题三:双写一致性,先动缓存 Redis 还是先动数据库 MySQL ?原因是什么?

问题四:延时双删了解过吗?怎么做?

问题五:Redis 和 MySQL双写一定会出现纰漏,虽然做不到强一致性,但可以做到最终一致性,怎么做?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/75834.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

“征服HTML引号恶魔:“完全解析手册”!!!(quot;表示双引号)

&#x1f6a8;&#x1f4e2; "征服HTML引号恶魔&#xff1a;“完全解析手册” &#x1f4e2;&#x1f6a8; &#x1f3af; 博客引言&#xff1a;当引号变成"恶魔" &#x1f631; 是否遇到过这种情况&#xff1a; 写HTML时满心欢喜输入<div title"他…

npm install 卡在创建项目:sill idealTree buildDeps

参考&#xff1a; https://blog.csdn.net/PengXing_Huang/article/details/136460133 或者再执行 npm install -g cnpm --registryhttps://registry.npm.taobao.org 或者换梯子

Pytorch学习笔记(十二)Learning PyTorch - NLP from Scratch

这篇博客瞄准的是 pytorch 官方教程中 Learning PyTorch 章节的 NLP from Scratch 部分。 官网链接&#xff1a;https://pytorch.org/tutorials/intermediate/nlp_from_scratch_index.html 完整网盘链接: https://pan.baidu.com/s/1L9PVZ-KRDGVER-AJnXOvlQ?pwdaa2m 提取码: …

mysql--socket报错

错误原因分析 MySQL 服务未运行&#xff08;最常见原因&#xff09; 错误中的 (2) 表示 “No such file or directory”&#xff0c;即 /tmp/mysql.sock 不存在这通常意味着 MySQL 服务器根本没有启动 socket 文件路径不匹配 客户端尝试连接 /tmp/mysql.sock但 MySQL 服务器可…

labview加载matlab数据时报错提示:对象引用句柄无效。

1. labview报错提示 labview加载mat数据时报错提示&#xff1a;对象引用句柄无效。返回该引用句柄的节点可能遇到错误&#xff0c;并没有返回有效的引用句柄。该引用句柄所指的存储可能在执行调用之前已关闭。报错提示如下&#xff1a; 这是由于labview缺少matlab MathWorks导…

20250330 Pyflink with Paimon

1. 数据湖 2. 本地安装Pyflink和Paimon 必须安装Python 3.11 Pip install python -m pip install apache-flink1.20.1 需要手动加入这两个jar 测试代码&#xff1a; import argparse import logging import sys import timefrom pyflink.common import Row from pyflink.tab…

-PHP 应用SQL 盲注布尔回显延时判断报错处理增删改查方式

#PHP-MYSQL-SQL 操作 - 增删改查 1 、功能&#xff1a;数据查询(对数据感兴趣&#xff09; 查询&#xff1a; SELECT * FROM news where id$id 2 、功能&#xff1a;新增用户&#xff0c;添加新闻等&#xff08;对操作的结果感兴趣&#xff09; 增加&#xff1a; INSERT INT…

【学习记录】大模型微调之使用 LLaMA-Factory 微调 Qwen系列大模型,可以用自己的数据训练

一、LoRA微调的基本原理 1、基本概念 LoRA&#xff08;Low-Rank Adaptation&#xff09;是一种用于大模型微调的技术&#xff0c;通过引入低秩矩阵来减少微调时的参数量。在预训练的模型中&#xff0c;LoRA通过添加两个小矩阵B和A来近似原始的大矩阵ΔW&#xff0c;从而减少需…

vulntarget_a 训练笔记

win 7 权限 利用任意文件上传 getshell POST /module/ueditor/php/action_upload.php?actionuploadfile HTTP/1.1 User-Agent: Mozilla/5.0 (compatible; Baiduspider/2.0; http://www.baidu.com/search/spider.html) Accept: */* Accept-Language: zh-CN,zh;q0.9 Connectio…

无人机螺旋桨平衡标准

螺旋桨平衡是确保无人机(UAV)平稳运行、可靠性和使用寿命的关键过程。螺旋桨的不平衡会导致振动、噪音&#xff0c;并加速关键部件的磨损&#xff0c;从而对飞行性能产生负面影响。 ISO 21940-11:2016标准为旋翼平衡提供了一个广泛引用的框架&#xff0c;定义了可接受的不平衡…

既生瑜何生亮?Nginx RTMP 模块与 SRS RTMP服务器技术对比

在实时视频流的场景中&#xff0c;RTMP 协议作为一种传统且高效的流媒体传输协议&#xff0c;广泛应用于各类直播和点播系统。两款流行的开源 RTMP 服务器分别是基于 Nginx 的 Nginx RTMP 模块 和 SRS&#xff08;Simple Real-Time Server&#xff09;。这两者都在流媒体行业有…

MATLAB 批量移动 TIF 文件至分类文件夹

文章目录 前言一、步骤二、代码 前言 本代码用于从指定的源文件夹 (sourceFolder) 中筛选所有 .tif 文件&#xff0c;并根据文件名的特定关键词&#xff08;Daynight 和 FDI&#xff09;将其分类移动到相应的目标文件夹 (targetDaynightFolder 和 targetFDIFolder)。 一、步骤…

基于Kubernetes部署Prometheus监控平台

#作者&#xff1a;stackofumbrella 文章目录 prometheus和k8s集群版本对照表架构Prometheus Operator简介kube-prometheus下载地址 安装修改镜像地址修改Prometheus的service修改Grafana的service修改Alertmanager的service数据持久化执行安装 Prometheus验证Grafana验证解决C…

STM32基础教程——输入捕获模式测量PWM频率

目录 前言 技术实现 原理图 连线图 代码实现 内容要点 PWM基本结构 开启外设时钟 配置GPIO端口 配置时基单元 初始化输出比较单元 输出比较通道重映射 输入捕获功能初始化 计算捕获PWM的频率 实验结果 问题记录 前言 IC&#xff08;Input Capture&#xff09;输…

基于网启PXE服务器的批量定制系统平台(详细版)

项目说明 该项目共分为2个子项目&#xff0c;由iventoy和定制安装两部分组成 该项目旨在复习巩固系统服务部署使用、shell编程等知识&#xff0c;旨在让学生增加知识面&#xff0c;提高项目实习经历&#xff0c;充实简历 项目背景&#xff1a; 公司新购了一批服务器和台式机…

旅游CMS选型:WordPress、Joomla与Drupal对比

内容概要 在旅游行业数字化转型进程中&#xff0c;内容管理系统&#xff08;CMS&#xff09;的选择直接影响网站运营效率与用户体验。WordPress、Joomla和Drupal作为全球主流的开源CMS平台&#xff0c;其功能特性与行业适配性存在显著差异。本文将从旅游企业核心需求出发&…

el-table下的复选框关联勾选

效果展示&#xff1a; <el-table style"height: 500px;" :data"tableData" border empty-text"暂无数据" v-loading"loading":header-cell-style"{ text-align: center }" :cell-style"{ text-align: center }"…

解决Cubemx生产的 .ioc文件不能外部打开的方法

正常来说&#xff0c;cubemx生成的文件会有图标 但是当图标白色的时候&#xff0c;无法通过直接点击这个文件进入cubemx 1.首先检查java环境是不是装的JAVA8&#xff0c;如果是的话进行第二步操作&#xff1b; 2.重新安装一次cubemx&#xff0c;在安装的时候选择为我安装&…

从零构建大语言模型全栈开发指南:第三部分:训练与优化技术-3.1.3分布式数据加载与并行处理(PyTorch DataLoader优化)

👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 3.1.3 分布式数据加载与并行处理(`PyTorch DataLoader`优化)1. 大规模数据加载的挑战与瓶颈分析1.1 数据加载流程的时间分解2. PyTorch DataLoader的深度优化策略2.1 核心参数调优2.2 分布式数据分片策…

K8S学习之基础五十七:部署代码扫描工具sonarqube

部署代码扫描工具sonarqube 拉取postgres、sonarqube镜像&#xff0c;在harbor上创建postgres、sonarqube项目&#xff0c;将镜像上传至harbordocker pull postgres docker pull sonarqube docker tat postgres:latest 172.16.80.140/postgres/postgres:latest docker tat sona…