1、【Java面试】70%的Java求职者都被问到过, Mysql中MyISAM和InnoDB引擎有什么区别?_哔哩哔哩_bilibili
# MyISAM和InnoDB引擎区别全面解析:面试必问
## 引言
**请简单说明一下,MySQL里面MyISAM和InnoDB引擎的一个区别。**
屏幕前有多少同学在面试过程中遇到了类似的问题啊,可以在评论区给我留言遇到过。
Hello大家好,我是Mike,一个工作了14年的**Java程序员**。关于这个问题的解析和高手的回答我已经把它整理到了一个20万字的面试文档里面。大家可以在我的主页去领取。
那么这个问题面试官想考察什么,以及我们又应该怎么样去正确回答呢?
对于某某类技术的一个区别,在面试过程中是一个非常常见的问题。一般情况下,面试官会通过这一类的问题来热场,打开接下来的沟通话题,然后沿着你的回答的内容再层层递进去做更深入的问题。
当然另外一个更加**深层次**的原因就是考察求职者对于这两个技术的理解层次,因为能够通过自己的理解去总结出他们的区别,至少说明你是有比较深入的研究的。
那么这个问题考察难度算是比较大的,一般面向三年以上开发经验的同学。
## 一、存储引擎概述
### 1.1 什么是存储引擎
**MyISAM**和**InnoDB**都是**MySQL**里面的两个存储引擎。
在MySQL里面,存储引擎是可以自己扩展的,它的本质其实就是定义数据的存储方式以及数据的读取的实现逻辑。
而不同的存储引擎本身的特性使得我们可以在针对性的去选择合适的引擎来实现不同的业务场景,从而去获取更好的性能。
### 1.2 默认存储引擎的演变
在**MySQL 5.5之前**,默认的存储引擎是**MyISAM**。
在**5.5以后**,**InnoDB**就作为了默认的存储引擎。
在实际开发里面呢,我们基本上都是采用**InnoDB**。
## 二、MyISAM引擎详解
### 2.1 数据存储方式
我们先来看一下**MyISAM**引擎。
**MyISAM引擎的数据是通过二进制的方式存储在磁盘上**,它的磁盘体现为两个文件:
1. **.MYD文件**(D代表Data):是MyISAM的数据文件,存放的是数据记录
2. **.MYI文件**(I代表Index):是MyISAM的索引文件,存放的是索引
### 2.2 存储结构
**这个是它的整体实现机制:**
因为索引和数据是分离的,所以在进行查找的时候:
1. 先从索引文件去找到数据的磁盘位置
2. 再到数据文件里面去找到索引对应的数据内容
**MyISAM存储结构:**
```
表名.MYD (数据文件)
├─ 数据记录1
├─ 数据记录2
└─ 数据记录3
表名.MYI (索引文件)
├─ 索引1 → 指向数据文件位置
├─ 索引2 → 指向数据文件位置
└─ 索引3 → 指向数据文件位置
```
**查找过程:**
```
查询请求
↓
查找索引文件(.MYI)
↓
找到数据在数据文件(.MYD)中的位置
↓
从数据文件中读取数据
```
## 三、InnoDB引擎详解
### 3.1 数据存储方式
在**InnoDB存储引擎**里面,数据同样是存储在磁盘上。
它的磁盘上,只有一个**.ibd文件**,里面包含索引和数据。
这个是它的一个整体结构。
### 3.2 存储结构
在**B+树**的叶子节点里面存储了索引对应的数据。
在通过索引进行检索的时候,**定位**到叶子节点就可以直接从叶子节点里面去读取数据行。
**InnoDB存储结构:**
```
表名.ibd (索引+数据文件)
├─ B+树索引结构
│ ├─ 非叶子节点:存储索引
│ └─ 叶子节点:存储索引+数据
└─ 数据行直接存储在叶子节点中
```
**查找过程:**
```
查询请求
↓
通过B+树索引查找
↓
定位到叶子节点
↓
直接从叶子节点读取数据(索引和数据在一起)
```
## 四、MyISAM和InnoDB的区别
了解这两个存储引擎以后,我们在面试的时候该怎么回答呢?
基于我的理解,我认为**MyISAM**和**InnoDB**的区别有四个:
### 4.1 区别一:数据存储方式不同
**第一个是数据存储的方式不同。**
- **MyISAM**:里面的数据和索引是分开存储的
- 数据存储在.MYD文件中
- 索引存储在.MYI文件中
- **InnoDB**:是把索引和数据存储在同一个文件里面
- 索引和数据都存储在.ibd文件中
- 数据直接存储在B+树的叶子节点中
**对比图示:**
```
MyISAM:
┌─────────┐ ┌─────────┐
│ .MYD │ │ .MYI │
│ (数据) │ │ (索引) │
└─────────┘ └─────────┘
分离存储
InnoDB:
┌─────────────┐
│ .ibd │
│ 索引+数据 │
│ (B+树结构) │
└─────────────┘
统一存储
```
### 4.2 区别二:事务支持不同
**第二个,对于事务的支持不同。**
- **MyISAM**:**不支持事务**
- 无法保证ACID特性
- 不适合需要事务的场景
- **InnoDB**:支持**ACID特性**的一个事务存储
- 支持事务的原子性、一致性、隔离性、持久性
- 适合需要事务的场景
**示例:**
```sql
-- MyISAM:不支持事务
CREATE TABLE users_myisam (
id INT PRIMARY KEY,
name VARCHAR(100)
) ENGINE=MyISAM;
-- 无法使用事务
BEGIN; -- 不支持
INSERT INTO users_myisam VALUES (1, '用户1');
ROLLBACK; -- 不支持回滚
-- InnoDB:支持事务
CREATE TABLE users_innodb (
id INT PRIMARY KEY,
name VARCHAR(100)
) ENGINE=InnoDB;
-- 可以使用事务
BEGIN;
INSERT INTO users_innodb VALUES (1, '用户1');
ROLLBACK; -- 支持回滚
```
### 4.3 区别三:锁支持不同
**第三个,对于锁的支持不同。**
- **MyISAM**:只支持**表锁**
- 锁定整个表
- 并发性能差
- **InnoDB**:可以根据不同的情况去支持:
- **行锁**:锁定单行记录
- **表锁**:锁定整个表
- **间隙锁**:锁定索引间隙
- **临键锁**:行锁+间隙锁的组合
**锁的对比:**
| 锁类型 | MyISAM | InnoDB |
|--------|--------|--------|
| 表锁 | ✅ 支持 | ✅ 支持 |
| 行锁 | ❌ 不支持 | ✅ 支持 |
| 间隙锁 | ❌ 不支持 | ✅ 支持 |
| 临键锁 | ❌ 不支持 | ✅ 支持 |
**并发性能对比:**
```sql
-- MyISAM:表锁
-- 事务A
LOCK TABLE users_myisam WRITE;
SELECT * FROM users_myisam WHERE id = 1;
-- 整个表被锁定
-- 事务B(被阻塞)
SELECT * FROM users_myisam WHERE id = 2; -- 被阻塞,等待表锁释放
-- InnoDB:行锁
-- 事务A
BEGIN;
SELECT * FROM users_innodb WHERE id = 1 FOR UPDATE;
-- 只锁定id=1这一行
-- 事务B(可以执行)
SELECT * FROM users_innodb WHERE id = 2 FOR UPDATE; -- 可以执行,只锁定id=2
```
### 4.4 区别四:外键支持不同
**第四种,MyISAM不支持外键,InnoDB支持外键。**
- **MyISAM**:不支持外键
- 无法在数据库层面保证引用完整性
- 需要在应用层保证数据一致性
- **InnoDB**:支持外键
- 可以在数据库层面保证引用完整性
- 自动维护外键约束
**示例:**
```sql
-- MyISAM:不支持外键
CREATE TABLE orders_myisam (
id INT PRIMARY KEY,
user_id INT,
-- FOREIGN KEY (user_id) REFERENCES users(id) -- 不支持
) ENGINE=MyISAM;
-- InnoDB:支持外键
CREATE TABLE orders_innodb (
id INT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id) -- 支持外键
) ENGINE=InnoDB;
```
## 五、区别总结对比表
| 特性 | MyISAM | InnoDB |
|------|--------|--------|
| **数据存储** | 数据和索引分离(.MYD和.MYI) | 数据和索引统一(.ibd) |
| **事务支持** | ❌ 不支持 | ✅ 支持ACID |
| **锁机制** | 只支持表锁 | 支持行锁、表锁、间隙锁、临键锁 |
| **外键支持** | ❌ 不支持 | ✅ 支持 |
| **并发性能** | 低(表锁) | 高(行锁) |
| **崩溃恢复** | 较差 | 较好(Redo Log) |
| **全文索引** | ✅ 支持 | MySQL 5.6+支持 |
| **适用场景** | 读多写少、不需要事务 | 需要事务、高并发 |
## 六、实际应用场景选择
因此,基于这些特性,我们可以在实际应用中可以根据不同的场景去选择合适的存储引擎。
### 6.1 选择InnoDB的场景
**如果需要去支持事务,那必须要去选择InnoDB。**
**适用场景:**
1. **需要事务支持**:
- 金融支付系统
- 订单系统
- 库存管理系统
2. **高并发写入**:
- 需要行锁支持
- 需要更好的并发性能
3. **需要外键约束**:
- 需要数据库层面保证数据完整性
4. **需要崩溃恢复**:
- 需要Redo Log保证数据安全
### 6.2 选择MyISAM的场景
**如果大部分的表操作都是查询,可以选择MyISAM。**
**适用场景:**
1. **读多写少**:
- 日志表
- 统计表
- 历史数据表
2. **不需要事务**:
- 对数据一致性要求不高的场景
3. **全文索引**:
- MySQL 5.6之前需要全文索引的场景
4. **简单查询**:
- 简单的SELECT查询
- 不需要复杂的并发控制
### 6.3 现代开发建议
**在现代开发中,建议:**
1. **默认使用InnoDB**:
- MySQL 5.5+默认引擎
- 支持事务,保证数据一致性
- 支持行锁,并发性能好
2. **谨慎使用MyISAM**:
- 除非有特殊需求(如全文索引)
- 大多数场景InnoDB都能满足需求
3. **根据业务选择**:
- 需要事务 → InnoDB
- 读多写少且不需要事务 → 可以考虑MyISAM
- 高并发 → InnoDB
## 七、性能对比
### 7.1 读取性能
**MyISAM:**
- 索引和数据分离
- 需要两次IO(索引文件+数据文件)
- 适合顺序读取
**InnoDB:**
- 索引和数据在一起
- 一次IO即可(B+树叶子节点)
- 适合随机读取
### 7.2 写入性能
**MyISAM:**
- 表锁,并发写入性能差
- 适合单线程写入
**InnoDB:**
- 行锁,并发写入性能好
- 适合多线程并发写入
### 7.3 事务性能
**MyISAM:**
- 不支持事务
- 无法保证ACID特性
**InnoDB:**
- 支持事务
- 通过Undo Log和Redo Log保证ACID
## 八、总结
### 8.1 核心区别
**MyISAM和InnoDB的四个核心区别:**
1. **数据存储方式**:MyISAM分离存储,InnoDB统一存储
2. **事务支持**:MyISAM不支持,InnoDB支持ACID
3. **锁支持**:MyISAM只支持表锁,InnoDB支持多种锁
4. **外键支持**:MyISAM不支持,InnoDB支持
### 8.2 选择建议
**实际应用中的选择:**
- **需要事务** → 必须选择InnoDB
- **高并发** → 选择InnoDB(行锁)
- **读多写少且不需要事务** → 可以考虑MyISAM
- **现代开发** → 默认使用InnoDB
### 8.3 面试回答要点
在回答这个问题时,需要说明:
1. **存储方式的区别**:分离存储 vs 统一存储
2. **事务支持的区别**:不支持 vs 支持ACID
3. **锁机制的区别**:表锁 vs 多种锁
4. **外键支持的区别**:不支持 vs 支持
5. **适用场景的区别**:根据业务需求选择
**核心思想:**
- 能够通过自己的理解去总结出他们的区别,说明有比较深入的研究
- 根据业务场景选择合适的存储引擎
- 现代开发中,InnoDB是主流选择
通过深入理解MyISAM和InnoDB的区别,我们可以在面试中给出让面试官满意的答案,并在实际开发中做出正确的技术选型。
2、数据量大做分库分表,为什么不可以直接使用MySQL自带分区表?_哔哩哔哩_bilibili
# 分区表 vs 分库分表:如何选择正确的数据分片方案
## 引言
我这边其实有一些疑问,就是我现在**有个疑问**:就是我们现在有个项目是要去做**分库分表**,因为现在的**数据量**比较多。
因为我看网上那个**MySQL数据库**它是自带的那种分区表的功能,但是现在的话,我项目里面也很多其他的同事的话,你就觉得要引入一些那种**MyCat**或者其他的一些**中间件**。
那我是也是一个疑问的,那我这个分区表为什么不能直接用呢?
## 一、分区表 vs 分库分表
### 1.1 分区表的特点
**分区表(Partition Table)**是MySQL自带的功能,可以将一个大表按照某种规则分成多个分区。
**分区表的实现:**
- 在同一个物理数据库中
- 表在逻辑上做了分区
- 但在物理上还是在同一个库上
**分区表示例:**
```sql
-- 创建分区表(按时间分区)
CREATE TABLE orders (
id INT,
order_date DATE,
amount DECIMAL(10,2)
) PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2020 VALUES LESS THAN (2021),
PARTITION p2021 VALUES LESS THAN (2022),
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024)
);
```
### 1.2 分库分表的特点
**分库分表**是将数据分散到多个物理数据库和多个表中。
**分库分表的实现:**
- 数据分散到多个物理数据库
- 数据分散到多个物理节点
- 需要中间件支持(如MyCat、Sharding-JDBC等)
**分库分表示例:**
```
数据库1 (物理节点1)
├─ orders_0
├─ orders_1
└─ orders_2
数据库2 (物理节点2)
├─ orders_3
├─ orders_4
└─ orders_5
```
## 二、为什么选择分库分表而不是分区表?
### 2.1 原因一:物理扩展性限制
**我看网上很多的方案也是很简单:因为你分区表还是在一个物理库上,虽然说你的表实现了物理的分表分库存储,但你还是在一个物理库上。**
**你后续如果要做迁移,比如说我想去实现多个库,我把数据分到多个库里面,分到多个物理节点上,你是做不到的。**
**所以你干脆一次性做到位。**
**如果你的数据量增长的趋势是比较明显,而且你未来的数据发展的预期你是可以清晰地感受到的,那么你还不如做物理上的分库。**
#### 分区表的限制
**分区表的问题:**
1. **单物理库限制**:
- 所有分区都在同一个物理数据库
- 无法跨物理节点扩展
- 单机性能瓶颈
2. **无法物理分离**:
- 无法将数据分散到多个物理服务器
- 无法实现真正的水平扩展
- 受限于单机硬件资源
3. **迁移困难**:
- 后续如果需要分库,需要大量数据迁移
- 迁移成本高,风险大
#### 分库分表的优势
**分库分表的优势:**
1. **物理扩展性**:
- 数据可以分散到多个物理数据库
- 可以分散到多个物理节点
- 真正的水平扩展
2. **性能提升**:
- 多个物理节点并行处理
- 突破单机性能瓶颈
- 提升整体吞吐量
3. **资源隔离**:
- 不同库可以部署在不同服务器
- 资源隔离,互不影响
- 故障隔离
**对比图示:**
```
分区表:
┌─────────────────────┐
│ 物理数据库1 │
│ ┌─────────────────┐ │
│ │ orders_p2020 │ │
│ │ orders_p2021 │ │
│ │ orders_p2022 │ │
│ │ orders_p2023 │ │
│ └─────────────────┘ │
└─────────────────────┘
所有分区在同一物理库
分库分表:
┌─────────────┐ ┌─────────────┐
│ 物理数据库1 │ │ 物理数据库2 │
│ orders_0 │ │ orders_3 │
│ orders_1 │ │ orders_4 │
│ orders_2 │ │ orders_5 │
└─────────────┘ └─────────────┘
数据分散到多个物理库
```
### 2.2 原因二:分库算法选择的重要性
**第二个,你在分库的时候你要想清楚你用什么样的分库算法。**
**就你一定要考虑到后续有可能你的数据量增加,我原来的分配方式。**
**举个例子,比如说我们用一个哈希取模的算法,就会导致你未来你发现你的分的这个表数量不够,那你还需要增加的时候,你就会需要大量数据迁移。这个很麻烦。**
#### 哈希取模算法的问题
**哈希取模算法:**
```java
// 哈希取模分库
int dbIndex = userId % 4; // 4个库
int tableIndex = userId % 8; // 每个库8个表
```
**问题:**
1. **扩容困难**:
- 从4个库扩展到8个库
- 需要重新计算所有数据的分库位置
- 需要大量数据迁移
2. **数据迁移成本高**:
- 需要迁移大量数据
- 迁移过程中可能影响业务
- 迁移风险大
3. **算法不可变**:
- 一旦确定算法,后续很难改变
- 改变算法需要全量数据迁移
#### 更好的分库算法
**1. 一致性哈希算法:**
```java
// 一致性哈希
// 优点:扩容时只需要迁移部分数据
// 缺点:实现复杂
```
**2. 范围分片:**
```java
// 范围分片
// userId: 0-1000万 → 库1
// userId: 1000万-2000万 → 库2
// 优点:扩容简单,只需要添加新库
// 缺点:可能数据分布不均匀
```
**3. 目录分片:**
```java
// 目录分片(路由表)
// 维护一个路由表,记录数据分布
// 优点:灵活,可以动态调整
// 缺点:需要维护路由表
```
## 三、分库分表的设计要点
### 3.1 分库分表策略选择
**在选择分库分表策略时,需要考虑:**
1. **数据增长预期**:
- 未来数据量增长趋势
- 是否需要频繁扩容
- 扩容的成本和风险
2. **查询模式**:
- 主要查询方式
- 是否需要跨库查询
- 查询性能要求
3. **业务特点**:
- 数据分布特点
- 热点数据分布
- 业务扩展性
### 3.2 分库分表算法对比
| 算法 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **哈希取模** | 数据分布均匀 | 扩容困难,需要大量迁移 | 数据量稳定,不需要频繁扩容 |
| **一致性哈希** | 扩容只需迁移部分数据 | 实现复杂 | 需要频繁扩容的场景 |
| **范围分片** | 扩容简单 | 可能数据分布不均匀 | 数据有明显范围特征 |
| **目录分片** | 灵活,可动态调整 | 需要维护路由表 | 数据分布复杂,需要灵活调整 |
### 3.3 分库分表的最佳实践
**1. 提前规划:**
- 根据业务发展预期提前规划
- 预留足够的扩展空间
- 考虑未来3-5年的数据增长
**2. 选择合适的算法:**
- 根据业务特点选择算法
- 考虑扩容成本和风险
- 优先选择扩容友好的算法
**3. 使用中间件:**
- 使用成熟的中间件(MyCat、Sharding-JDBC等)
- 减少开发工作量
- 提高系统稳定性
**4. 监控和优化:**
- 监控数据分布情况
- 及时发现热点数据
- 优化分片策略
## 四、分区表的适用场景
### 4.1 什么时候可以使用分区表
虽然分库分表有优势,但分区表也有其适用场景:
**1. 数据量不是特别大:**
- 单机可以承受的数据量
- 不需要跨物理节点扩展
**2. 主要是时间维度分区:**
- 按时间分区,历史数据可以归档
- 查询主要针对最近的数据
**3. 不需要跨库查询:**
- 查询都在单个库内
- 不需要分布式事务
**4. 运维简单:**
- 不需要引入中间件
- 运维成本低
### 4.2 分区表的优势
**分区表的优势:**
1. **实现简单**:MySQL自带功能,无需中间件
2. **运维成本低**:不需要管理多个物理库
3. **适合时间分区**:按时间分区,历史数据归档方便
4. **查询优化**:MySQL可以自动优化分区查询
## 五、总结
### 5.1 选择原则
**选择分区表还是分库分表,需要根据实际情况:**
**选择分库分表的情况:**
1. **数据量增长明显**:未来数据量会大幅增长
2. **需要物理扩展**:需要跨物理节点扩展
3. **高并发场景**:需要多个物理节点提升性能
4. **长期规划**:需要长期支持业务发展
**选择分区表的情况:**
1. **数据量适中**:单机可以承受
2. **时间分区**:主要是按时间维度分区
3. **简单场景**:不需要复杂的分布式能力
4. **运维简单**:希望运维成本低
### 5.2 核心建议
**核心建议:**
1. **提前规划**:根据业务发展预期提前规划
2. **选择合适算法**:考虑扩容成本和风险
3. **使用中间件**:使用成熟的中间件减少开发工作量
4. **预留扩展空间**:为未来扩展预留空间
### 5.3 关键点
**两个关键点:**
1. **物理扩展性**:如果未来需要跨物理节点扩展,选择分库分表
2. **分库算法**:选择扩容友好的算法,避免大量数据迁移
**记住:**
- 分区表适合数据量不是特别大,不需要跨物理节点扩展的场景
- 分库分表适合数据量增长明显,需要物理扩展的场景
- 选择分库算法时,要考虑未来扩容的成本和风险
通过合理选择数据分片方案,我们可以在保证系统性能的同时,为未来的扩展做好准备。
3、B站回答最好的MySQL面试题!这么回答轻松拿捏面试官【Java面试】_哔哩哔哩_bilibili
# Redis和MySQL数据一致性保证方案:最终一致性实战
## 引言
今天我们来分享一道一线互联网公司的高频**面试题**。它的题目是:**Redis和MySQL如何去保证数据的一致性?**
这个问题难倒了不少工作五年以上的**Java程序员**,难得不是问题的本身,而是解决这个问题的思维模式。
下面我们来看看高手对于这个问题的回答。
## 一、架构背景
### 1.1 Redis缓存架构
**高手的回答:**
一般情况下,Redis是用来实现应用和数据库之间的一个**读操作**的**缓存层**。它主要目的是去减少数据库的IO,还可以提升数据的IO性能。
这个是它的一个整体架构。
**缓存架构流程:**
```
应用程序
↓
读取请求
↓
├─ 先尝试从Redis加载
│ ├─ 命中 → 直接返回
│ └─ 未命中 → 从数据库查询
│ ↓
│ 查询到数据之后再把数据缓存到Redis里面
└─ 返回数据
```
当应用程序需要去读取某个数据的时候:
1. 首先会先尝试Redis里面去**加载**
2. 如果命中了,就直接返回
3. 如果没有命中,就直接从数据库里面查询
4. **查询到数据之后再把数据缓存到Redis里面**
### 1.2 数据一致性问题
在这个架构里面会出现一个问题:**一份数据同时保存在数据库和Redis里面。**
当数据发生变化的时候,需要同时去更新Redis和**MySQL**。
由于更新操作是有先后顺序的,并且它并不像MySQL中的多表事务操作,可以满足ACID的特性。所以就会出现一个叫**数据一致性**的问题。
## 二、数据一致性问题的原因
### 2.1 问题根源
**核心问题:**
- Redis和MySQL是两个独立的存储系统
- 更新操作不是原子性的
- 无法保证两个操作的ACID特性
- 并发场景下会出现数据不一致
### 2.2 典型场景
**场景1:更新顺序问题**
```
时间线:
T1: 更新MySQL成功
T2: 更新Redis失败
结果:MySQL是新数据,Redis是旧数据,不一致
```
**场景2:并发访问问题**
```
线程A: 更新MySQL → 更新Redis
线程B: 读取Redis(在A更新Redis之前)→ 读取到旧数据
结果:数据不一致
```
## 三、解决方案分析
在这个情况下,能够选择的方法只有几种:
### 3.1 方案一:先更新数据库,再更新缓存
**流程:**
```
1. 更新MySQL数据库
2. 更新Redis缓存
```
**优点:**
- 实现简单
- 数据库是数据源,优先保证数据库正确
**缺点:**
- **如果缓存更新失败,就会导致数据库和Redis里面的数据是不一致的**
- 并发场景下可能出现数据不一致
**问题场景:**
```
时间线:
T1: 线程A更新MySQL(数据=100)
T2: 线程B更新MySQL(数据=200)
T3: 线程B更新Redis(数据=200)
T4: 线程A更新Redis(数据=100)
结果:MySQL=200,Redis=100,不一致
```
### 3.2 方案二:先删除缓存,再更新数据库
**流程:**
```
1. 删除Redis缓存
2. 更新MySQL数据库
```
**理想情况:**
如果是先删除缓存再更新数据库,理想情况下是应用下次访问Redis的时候发现Redis里面的数据是空的,那么就会从数据库加载保存到Redis里面,也就是说数据理论上是一致的。
**极端情况:**
但是在极端情况下,由于**删除Redis**和更新数据库这两个操作并不是**原子操作**,所以在这个过程中,如果出现其他线程来访问,还是会存在数据不一致的问题。
**问题场景:**
```
时间线:
T1: 线程A删除Redis缓存
T2: 线程B读取Redis(未命中)→ 从MySQL读取旧数据(数据=100)
T3: 线程A更新MySQL(数据=200)
T4: 线程B写入Redis(数据=100)
结果:MySQL=200,Redis=100,不一致
```
## 四、最终一致性方案
所以如果需要在极端情况下,仍然去保证Redis和MySQL的数据一致性,就只能采用**最终一致性**的一个方案。
### 4.1 方案一:基于消息队列的最终一致性
**方案:基于RocketMQ的可靠性消息通信,来实现数据的最终一致性。**
**架构:**
```
应用程序
↓
更新MySQL数据库
↓
发送消息到RocketMQ
↓
消息消费者
↓
更新Redis缓存
```
**流程:**
1. 应用程序更新MySQL数据库
2. 发送消息到RocketMQ(保证消息可靠性)
3. 消息消费者消费消息,更新Redis缓存
4. 如果更新失败,消息会重试,直到成功
**优点:**
- 解耦数据库和缓存更新
- 通过消息队列保证可靠性
- 支持重试机制
**缺点:**
- 存在延迟,不是强一致性
- 需要额外的消息队列组件
- 系统复杂度增加
**实现示例:**
```java
// 伪代码示例
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userMapper.update(user);
// 2. 发送消息到MQ
messageProducer.send("user-update", user);
}
// 消息消费者
@RocketMQMessageListener(topic = "user-update")
public class UserUpdateListener implements MessageListener {
@Override
public void consume(Message message) {
User user = JSON.parseObject(message.getBody(), User.class);
// 更新Redis缓存
redisTemplate.opsForValue().set("user:" + user.getId(), user);
}
}
```
### 4.2 方案二:基于Canal的binlog同步
**方案:直接通过Canal组件,监控MySQL里面的binlog日志,把更新后的数据同步到Redis里面。**
**架构:**
```
MySQL数据库
↓
binlog日志
↓
Canal组件(监控binlog)
↓
解析binlog变更
↓
更新Redis缓存
```
**流程:**
1. Canal监控MySQL的binlog日志
2. 当MySQL数据发生变化时,binlog会记录变更
3. Canal解析binlog,获取变更数据
4. 将变更数据同步到Redis
**优点:**
- 完全解耦,应用程序无需关心缓存更新
- 基于binlog,数据变更都能捕获
- 不影响业务代码
**缺点:**
- 需要部署Canal组件
- 存在延迟,不是强一致性
- 需要处理binlog解析的复杂性
**实现原理:**
```java
// Canal客户端伪代码
public class CanalClient {
public void process() {
// 连接Canal
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111),
"example", "", "");
connector.connect();
connector.subscribe(".*\\..*");
while (true) {
Message message = connector.getWithoutAck(100);
List<Entry> entries = message.getEntries();
for (Entry entry : entries) {
if (entry.getEntryType() == EntryType.ROWDATA) {
RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
// 解析变更数据
// 更新Redis缓存
}
}
connector.ack(message.getId());
}
}
}
```
### 4.3 最终一致性的特点
因为这里是基于最终一致性来实现的,**如果业务场景不能去接受数据的短期不一致性,那么就不能使用这样的方案来实现。**
**最终一致性的特点:**
- **不是强一致性**:在短时间内,Redis和MySQL的数据可能不一致
- **最终会一致**:通过异步同步,最终数据会达到一致
- **适合大多数场景**:大多数业务场景可以接受短期不一致
**不适合的场景:**
- 金融支付场景(需要强一致性)
- 库存扣减场景(需要强一致性)
- 对数据一致性要求极高的场景
## 五、面试技巧
### 5.1 反问业务场景
在面试的时候,面试官喜欢就问各种没有场景化的纯粹技术问题。比如说:
> "你这个最终一致性方案,还是会存在数据不一致性的问题啊,那怎么解决呢?"
**先不用慌。技术是为了业务服务的。所有不同的业务场景对于技术的选择和方案的设计都是不同的。**
所以在这个时候可以**反问面试官具体的业务场景是什么?**
**示例回答:**
> "这个问题很好。我想先了解一下具体的业务场景:
> - 这个数据的一致性要求有多高?
> - 是否可以接受短期的数据不一致?
> - 数据变更的频率如何?
> - 对性能的要求是什么?
>
> 因为不同的业务场景,我们选择的方案是不同的。比如:
> - 如果是用户昵称更新,可以接受短期不一致,使用最终一致性方案
> - 如果是库存扣减,需要强一致性,可能需要使用分布式事务或者直接操作数据库
> - 如果是商品信息展示,可以接受最终一致性,使用Canal同步方案"
### 5.2 核心思想
**一定要知道的是:一个技术方案不可能覆盖住所有的场景。**
**技术选型原则:**
- 根据业务场景选择合适的技术方案
- 没有银弹,没有完美的方案
- 需要在一致性、性能、复杂度之间权衡
- 优先考虑业务需求,再选择技术方案
## 六、方案选择指南
### 6.1 强一致性场景
**适用场景:**
- 金融支付
- 库存扣减
- 账户余额
**方案:**
- 使用分布式事务(如Seata)
- 直接操作数据库,不使用缓存
- 使用强一致性存储(如TiDB)
### 6.2 最终一致性场景
**适用场景:**
- 用户信息更新
- 商品信息更新
- 文章内容更新
**方案:**
- 基于消息队列的最终一致性
- 基于Canal的binlog同步
- 延迟双删
### 6.3 可接受不一致场景
**适用场景:**
- 统计数据
- 排行榜
- 热点数据
**方案:**
- 定期刷新缓存
- 设置缓存过期时间
- 简单的更新策略
## 七、最佳实践
### 7.1 设计原则
1. **优先保证数据库一致性**:数据库是数据源,优先保证数据库正确
2. **根据业务场景选择方案**:没有完美的方案,只有合适的方案
3. **考虑并发场景**:设计时要考虑多线程并发访问
4. **监控和告警**:建立监控机制,及时发现数据不一致
### 7.2 实施建议
1. **评估业务需求**:明确一致性要求
2. **选择合适方案**:根据场景选择方案
3. **设计降级策略**:缓存异常时的降级方案
4. **建立监控体系**:监控数据一致性
5. **定期演练**:定期进行故障演练
## 八、总结
Redis和MySQL数据一致性保证是一个复杂的问题,需要从多个维度来考虑:
### 8.1 核心要点
1. **理解问题本质**:两个独立存储系统,无法保证原子性
2. **分析各种方案**:先更新DB、先删除缓存、延迟双删等
3. **选择合适方案**:根据业务场景选择强一致性或最终一致性
4. **反问业务场景**:面试时要反问具体场景,不要盲目回答
### 8.2 核心思想
- **技术是为业务服务的**,没有完美的技术方案
- **不同的业务场景需要不同的解决方案**
- **需要在一致性、性能、复杂度之间权衡**
- **大多数场景可以接受最终一致性**
### 8.3 面试建议
**另外,最近从评论区收到了很多面试的问题都有点太泛了。比如说,你来说一下MongoDB,说一下Kafka,说一下并发,那么这些问题至少都需要几个小时才能说清楚。建议大家提问的时候可以具体一点。**
通过系统性的分析和合适的技术选型,我们可以为不同的业务场景选择最合适的数据一致性方案。