Redis 和 MySQL 的数据很难直接实现 强一致性,但可以通过一些策略尽量接近或实现 最终一致性。下面从两者的特性、挑战以及解决方案来分析。
Redis 和 MySQL 的特性
为什么 Redis 和 MySQL 难以实现强一致性?
(1) 两者的数据更新机制不同
(2) 分布式 CAP 理论限制
(3) 数据的写入顺序问题
为什么需要延时双删?
延时双删的伪代码
public void updateData(String key, Object newValue) {
// 1. 第一次删除缓存
redisTemplate.delete(key);
// 2. 更新数据库
databaseService.updateData(key, newValue);
// 3. 延时后再次删除缓存
new Thread(() -> {
try {
Thread.sleep(500); // 延时 500ms
redisTemplate.delete(key); // 第二次删除
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
延时双删策略主要适用于以下场景:
优点
缺点
优化策略
延时双删策略的延时时间主要取决于以下几个因素:
1. 数据库更新耗时
2. 系统的并发访问特性
如果系统的读请求频率较高,并发量大,容易在第一次删除缓存后、数据库更新完成前出现缓存重建的情况,需要适当延长延时时间。
3. 数据一致性的业务需求
4. 业务对延时的敏感性
推荐值
综合考虑,延时时间通常设置为 300ms~500ms,在大多数场景下是较为合理的起始值。如果业务对性能或一致性有特殊需求,可以进行以下调整:
如何精准确定延时
为了选择最佳延时,可以采用以下方法:
注意事项
通过合理选择延时时间,可以最大限度地降低延时双删策略的弊端,实现高效的一致性保障。
使用 消息队列 来保证 Redis 和 MySQL 数据一致性 是一种常见的解决方案,可以有效应对高并发场景中的数据同步问题。以下是该方法的具体流程、实现步骤及优缺点分析。
实现流程
假设业务场景需要更新 MySQL 数据,并保持 Redis 缓存一致性:
步骤 1:更新 MySQL 数据
步骤 2:消息队列的缓存同步任务
伪代码实现
生产者:更新 MySQL 和发送消息
生产者负责更新 MySQL 数据并发送消息到队列。
@Transactional
public void updateData(String key, Object newValue) {
// 1. 更新 MySQL 数据
databaseService.update(key, newValue);
// 2. 发送消息到消息队列
String message = "UPDATE_CACHE:" + key; // 构造更新缓存的消息
messageQueue.send(message); // 假设 messageQueue 是 MQ 的工具类
}
消费者:更新或删除 Redis 缓存
消费者负责监听消息队列并处理缓存同步。
@RabbitListener(queues = "cacheUpdateQueue") // 假设使用 RabbitMQ
public void handleCacheUpdate(String message) {
// 1. 解析消息
if (message.startsWith("UPDATE_CACHE:")) {
String key = message.split(":")[1];
// 2. 删除 Redis 缓存(或更新缓存)
redisTemplate.delete(key);
// 3. 如果需要,也可以重新加载 MySQL 数据到 Redis
Object newValue = databaseService.findByKey(key);
redisTemplate.opsForValue().set(key, newValue);
}
}
消息队列对一致性的保障
强一致性
通过消息队列的事务机制(如 RocketMQ 的事务消息、Kafka 的幂等消费机制),可以实现 Redis 和 MySQL 数据的强一致性。
最终一致性
通常,消息队列的方式更适合 最终一致性 的场景:
为什么消息队列可以保证一致性?
消息队列的事务处理
为了确保消息队列和 MySQL 操作的一致性,可以使用 事务消息机制:
解决 MySQL 和消息队列一致性的方法
优缺点分析
优点
缺点
适用场景
registry {
type = "nacos" # 使用 Nacos 作为注册中心
nacos {
serverAddr = "127.0.0.1:8848" # Nacos 服务器地址
namespace = "public" # Nacos 命名空间
cluster = "default" # Nacos 集群名
serviceName = "seata-server" # Seata Server 注册的服务名
}
}
store {
mode = "db" # 数据存储模式,这里选择数据库模式
db {
datasource {
driverClassName = "com.mysql.cj.jdbc.Driver" # MySQL 驱动
url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8" # 数据库连接 URL
user = "root" # 数据库用户名
password = "password" # 数据库密码
}
}
# Redis 配置部分(示例)
redis {
mode = "single" # Redis 单实例模式
single {
address = "127.0.0.1:6379" # Redis 服务器地址
password = "" # Redis 密码(如有)
database = 0 # 使用的数据库索引
}
}
}
Seata Client 的配置是在 application.yml 文件中的。
spring:
application:
name: spring-boot-seata-client # 应用程序名称
cloud:
alibaba:
seata:
tx-service-group: my_tx_group # 分布式事务服务组名称
seata:
enabled: true # 启用 Seata
application-id: spring-boot-seata-client # Seata Client 的应用 ID
tx-service-group: my_tx_group # 事务服务组,需与 Seata Server 配置一致
enable-auto-data-source-proxy: true # 启用自动数据源代理
client:
rm:
report-success-enable: true # 是否启用事务成功报告
transport:
type: "TCP" # 网络传输协议,通常为 TCP
group: "default" # 服务组名
thread-count: 8 # 线程数量
# 其他传输配置
创建一个服务类 OrderService 来演示分布式事务:
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
/**
* 使用 Seata 的 @GlobalTransactional 注解管理分布式事务
*/
@GlobalTransactional
public void createOrderAndUpdateStock(String orderId, String productId, int quantity) {
// Step 1: 更新 MySQL 中的订单
String insertOrderQuery = "INSERT INTO orders (order_id, product_id, quantity) VALUES (?, ?, ?)";
jdbcTemplate.update(insertOrderQuery, orderId, productId, quantity);
// Step 2: 更新 Redis 中的库存
String redisStockKey = "product_stock_" + productId;
Integer stock = redisTemplate.opsForValue().get(redisStockKey);
if (stock == null || stock < quantity) {
throw new RuntimeException("库存不足");
}
redisTemplate.opsForValue().set(redisStockKey, stock - quantity);
}
}
高并发读多写少场景(如电商商品信息缓存):
强一致性需求的场景(如账户余额管理):
性能优先的场景(如秒杀库存管理):
Redis 和 MySQL 天然不支持强一致性,尤其是高并发场景下的数据一致性管理需要权衡性能和一致性:
因篇幅问题不能全部显示,请点此查看更多更全内容