引言:理解Redis与数据库双向同步的核心价值
在现代高并发应用架构中,Redis作为内存数据库常被用作缓存层,而关系型数据库(如MySQL、PostgreSQL)则作为持久化存储层。双向同步机制指的是Redis与主数据库之间保持数据实时一致性的技术方案,这种机制不仅解决了缓存与数据库的一致性问题,还为高可用性提供了基础保障。
为什么需要双向同步?
想象一个电商网站的库存系统:用户下单时,库存数据需要在Redis缓存中快速扣减,同时必须持久化到MySQL中。如果只有单向同步(数据库→Redis),当Redis宕机时,缓存重建可能导致数据不一致;如果只有单向同步(Redis→数据库),则可能丢失未持久化的数据。双向同步确保了:
数据一致性
:无论从Redis还是数据库读取,数据始终保持同步
高可用性
:任一系统故障时,另一系统可继续提供服务
性能优化
:Redis提供毫秒级响应,数据库保证数据安全
一、双向同步的技术实现方案
1.1 基于Binlog的同步机制(推荐方案)
这是目前最成熟可靠的方案,通过监听数据库的二进制日志(Binary Log)来实现数据变更的捕获和同步。
1.1.1 MySQL Binlog同步到Redis的实现
原理
:使用Canal或Debezium等工具解析MySQL的binlog,将数据变更实时应用到Redis。
完整代码示例:使用Canal实现MySQL→Redis同步
// Canal客户端监听器实现
@Component
public class CanalEventListener {
@Autowired
private RedisTemplate
// 监听MySQL的binlog变更
@CanalEventListener
public void handleDataChange(CanalEntry.Event event) {
String tableName = event.getTableName();
// 只处理需要同步的表
if (!Arrays.asList(“product”, “inventory”, “user”).contains(tableName)) {
return;
}
// 解析binlog事件类型
switch (event.getType()) {
case CanalEntry.EventType.INSERT:
handleInsert(event);
break;
case CanalEntry.EventType.UPDATE:
handleUpdate(event);
break;
case CanalEntry.EventType.DELETE:
handleDelete(event);
break;
}
}
private void handleInsert(CanalEntry.Event event) {
List
for (CanalEntry.RowData rowData : rowDatasList) {
// 构建Redis键值对
String primaryKey = getPrimaryKey(rowData);
Map
// 写入Redis Hash
String redisKey = String.format(“%s:%s”, event.getTableName(), primaryKey);
redisTemplate.opsForHash().putAll(redisKey, dataMap);
// 设置过期时间(防止缓存雪崩)
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
// 发布变更事件到Redis Pub/Sub
redisTemplate.convertAndSend(“data:change”,
String.format(“INSERT: %s -> %s”, redisKey, dataMap));
}
}
private void handleUpdate(CanalEntry.Event event) {
List
for (CanalEntry.RowData rowData : rowDatasList) {
String primaryKey = getPrimaryKey(rowData);
Map
String redisKey = String.format(“%s:%s”, event.getTableName(), primaryKey);
// 先删除旧数据,再写入新数据(保证原子性)
redisTemplate.delete(redisKey);
redisTemplate.opsForHash().putAll(redisKey, newDataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
// 发布更新事件
redisTemplate.convertAndSend(“data:change”,
String.format(“UPDATE: %s -> %s”, redisKey, newDataMap));
}
}
private void handleDelete(CanalEntry.Event event) {
List
for (CanalEntry.RowData rowData : rowDatasList) {
String primaryKey = getPrimaryKey(rowData);
String redisKey = String.format(“%s:%s”, event.getTableName(), primaryKey);
// 删除Redis数据
redisTemplate.delete(redisKey);
// 发布删除事件
redisTemplate.convertAndSend(“data:change”,
String.format(“DELETE: %s”, redisKey));
}
}
// 辅助方法:获取主键
private String getPrimaryKey(CanalEntry.RowData rowData) {
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if (column.getIsKey()) {
return column.getValue();
}
}
throw new RuntimeException(“Primary key not found”);
}
// 辅助方法:转换行数据为Map
private Map
Map
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
dataMap.put(column.getName(), column.getValue());
}
return dataMap;
}
}
1.1.2 Redis数据变更同步回MySQL的实现
原理
:通过Redis的Key过期监听(Keyspace Notifications)或AOF日志解析,将Redis中的数据变更同步到MySQL。
完整代码示例:Redis→MySQL同步服务
@Component
public class RedisToMySQLSyncService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedisTemplate
// 监听Redis Key过期事件
@RedisListener
public void handleKeyExpiration(String message) {
// message格式: “expired product:123”
if (message.startsWith(“expired “)) {
String[] parts = message.split(” “);
if (parts.length >= 2) {
String redisKey = parts[1];
syncToMySQL(redisKey, “EXPIRE”);
}
}
}
// 监听Redis数据变更(通过Redis Stream或消息队列)
@Scheduled(fixedDelay = 1000) // 每秒执行一次批量同步
public void batchSyncFromRedisToMySQL() {
Set
if (keys == null || keys.isEmpty()) return;
List
// 清除同步标记
connection.del((“sync:” + key).getBytes());
}
}
}
return null;
});
}
// 手动触发同步(在写入Redis时调用)
public void writeWithSync(String key, Map
// 1. 写入Redis
redisTemplate.opsForHash().putAll(key, data);
redisTemplate.expire(key, 3600, TimeUnit.SECONDS);
// 2. 设置同步标记
redisTemplate.opsForValue().set(“sync:” + key, “1”, 30, TimeUnit.SECONDS);
// 3. 立即同步到MySQL(异步)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(100); // 稍微延迟,避免频繁写入
syncHashToMySQL(key, data);
redisTemplate.delete(“sync:” + key);
} catch (Exception e) {
log.error(“Sync failed for key: {}”, key, e);
// 失败时记录到死信队列
redisTemplate.opsForList().leftPush(“sync:failed”, key);
}
});
}
private void syncHashToMySQL(String redisKey, Map
String[] keyParts = redisKey.split(“:”);
if (keyParts.length < 2) return;
String tableName = keyParts[0];
String id = keyParts[1];
// 构建SQL
StringBuilder sql = new StringBuilder("INSERT INTO ")
.append(tableName).append(" (");
List
List
for (Map.Entry
columns.add(entry.getKey().toString());
sql.append(“?”).append(“,”);
params.add(entry.getValue());
}
// 移除最后一个逗号
sql.deleteCharAt(sql.length() – 1);
sql.append(“) VALUES (“);
for (int i = 0; i < columns.size(); i++) {
sql.append("?,");
}
sql.deleteCharAt(sql.length() - 1);
sql.append(") ON DUPLICATE KEY UPDATE ");
// 添加更新部分
for (int i = 0; i < columns.size(); i++) {
if (i > 0) sql.append(“,”);
sql.append(columns.get(i)).append(“=VALUES(“).append(columns.get(i)).append(“)”);
}
try {
jdbcTemplate.update(sql.toString(), params.toArray());
log.info(“Successfully synced {} to MySQL”, redisKey);
} catch (Exception e) {
log.error(“Failed to sync {} to MySQL”, redisKey, e);
throw new RuntimeException(“MySQL sync failed”, e);
}
}
// 处理同步失败的重试机制
@Scheduled(fixedDelay = 60000) // 每分钟检查一次失败队列
public void handleFailedSyncs() {
String failedKey = “sync:failed”;
Long listSize = redisTemplate.opsForList().size(failedKey);
if (listSize == null || listSize == 0) return;
for (int i = 0; i < Math.min(10, listSize); i++) {
String redisKey = redisTemplate.opsForList().rightPop(failedKey);
if (redisKey != null) {
Map
if (!data.isEmpty()) {
try {
syncHashToMySQL(redisKey, data);
log.info(“Retry sync success for: {}”, redisKey);
} catch (Exception e) {
// 重新放入队列,等待下次重试
redisTemplate.opsForList().leftPush(failedKey, redisKey);
log.warn(“Retry sync failed again for: {}”, redisKey);
}
}
}
}
}
}
1.2 基于Redis Pub/Sub的同步机制
原理
:利用Redis的发布/订阅功能,当应用层写入Redis时,同时发布变更消息,消费者订阅消息并同步到数据库。
代码示例:应用层双写模式
@Service
public class DualWriteService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private KafkaTemplate
/**
* 双写模式:同时写入Redis和数据库
* 保证原子性的事务方案
*/
@Transactional
public void saveProduct(Product product) {
try {
// 1. 先写入MySQL(持久化优先)
String sql = “INSERT INTO product (id, name, price, stock) VALUES (?, ?, ?, ?) ” +
“ON DUPLICATE KEY UPDATE name=?, price=?, stock=?”;
jdbcTemplate.update(sql,
product.getId(), product.getName(), product.getPrice(), product.getStock(),
product.getName(), product.getPrice(), product.getStock());
// 2. 写入Redis
String redisKey = “product:” + product.getId();
Map
dataMap.put(“id”, product.getId());
dataMap.put(“name”, product.getName());
dataMap.put(“price”, product.getPrice());
dataMap.put(“stock”, product.getStock());
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
// 3. 发布变更事件(用于其他消费者)
String event = String.format(“%s:%s:%s”, “product”, product.getId(), “UPDATE”);
redisTemplate.convertAndSend(“product:change”, event);
// 4. 发送到消息队列(用于异步处理)
kafkaTemplate.send(“product-updates”, product.getId().toString(), event);
} catch (Exception e) {
// 异常时回滚MySQL事务,Redis写入失败会自动回滚
log.error(“Failed to save product: {}”, product.getId(), e);
throw new RuntimeException(“Save failed”, e);
}
}
/**
* 基于Redis Stream的可靠同步方案
* Redis 5.0+ 提供的可靠消息队列
*/
public void saveProductWithStream(Product product) {
// 1. 写入Redis
String redisKey = “product:” + product.getId();
Map
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
// 2. 写入Redis Stream(保证消息不丢失)
Map
streamData.put(“operation”, “UPDATE”);
streamData.put(“table”, “product”);
streamData.put(“data”, JSON.toJSONString(dataMap));
// 添加到Stream,消息ID由Redis生成
RecordID recordId = redisTemplate.opsForStream().add(“sync:stream”, streamData);
// 3. 消费者处理Stream消息
// 见下方的Stream消费者代码
}
// Redis Stream消费者(独立线程)
@PostConstruct
public void startStreamConsumer() {
new Thread(() -> {
while (true) {
try {
// 读取Stream消息
List
redisTemplate.opsForStream().read(
Consumer.from(“sync-consumer-group”, “consumer-1”),
StreamReadOptions.empty().count(10).block(Duration.ofSeconds(1)),
StreamOffset.create(“sync:stream”, ReadOffset.lastConsumed())
);
for (StreamMessage
processStreamMessage(message);
// 确认消息已处理
redisTemplate.opsForStream().acknowledge(“sync:stream”, “sync-consumer-group”, message.getId());
}
} catch (Exception e) {
log.error(“Stream consumer error”, e);
Thread.sleep(1000);
}
}
}).start();
}
private void processStreamMessage(StreamMessage
Map
String operation = data.get(“operation”);
String table = data.get(“table”);
String jsonData = data.get(“data”);
// 解析并同步到MySQL
Map
syncHashToMySQL(table + “:” + rowData.get(“id”), rowData);
}
}
二、数据一致性保障机制
2.1 最终一致性模型
核心思想
:允许短暂的不一致,通过补偿机制最终达到一致。
实现方案:基于消息队列的异步同步
/**
* 最终一致性保障:使用消息队列解耦
* 1. 写入Redis成功后,发送消息到MQ
* 2. 消费者从MQ读取消息,同步到MySQL
* 3. 失败重试 + 死信队列
*/
@Service
public class EventualConsistencyService {
@Autowired
private RedisTemplate
@Autowired
private KafkaTemplate
// 写入数据(保证Redis成功即可返回)
public void writeData(String key, Map
// 1. 写入Redis
redisTemplate.opsForHash().putAll(key, data);
redisTemplate.expire(key, 3600, TimeUnit.SECONDS);
// 2. 发送同步事件(异步,不阻塞主流程)
SyncEvent event = new SyncEvent();
event.setKey(key);
event.setData(data);
event.setTimestamp(System.currentTimeMillis());
event.setRetryCount(0);
// 发送到Kafka(topic: db-sync)
kafkaTemplate.send(“db-sync”, key, JSON.toJSONString(event));
}
// 消费者:同步到MySQL
@KafkaListener(topics = “db-sync”, groupId = “db-sync-group”)
public void syncToMySQL(String message) {
SyncEvent event = JSON.parseObject(message, SyncEvent.class);
try {
// 检查Redis中数据是否还存在(可能已被删除)
if (!redisTemplate.hasKey(event.getKey())) {
log.warn(“Key {} already expired, skip sync”, event.getKey());
return;
}
// 同步到MySQL
syncHashToMySQL(event.getKey(), event.getData());
log.info(“Sync success for key: {}”, event.getKey());
} catch (Exception e) {
log.error(“Sync failed for key: {}, retry count: {}”,
event.getKey(), event.getRetryCount(), e);
// 重试逻辑
if (event.getRetryCount() < 3) {
event.setRetryCount(event.getRetryCount() + 1);
// 延迟重试
kafkaTemplate.send("db-sync-delay",
event.getKey(),
JSON.toJSONString(event));
} else {
// 进入死信队列
kafkaTemplate.send("db-sync-dead-letter",
event.getKey(),
JSON.toJSONString(event));
log.error("Max retries exceeded for key: {}", event.getKey());
}
}
}
// 延迟队列消费者(处理重试)
@KafkaListener(topics = "db-sync-delay", groupId = "db-sync-delay-group")
public void handleDelayedSync(String message) {
SyncEvent event = JSON.parseObject(message, SyncEvent.class);
// 延迟一段时间后重试
try {
Thread.sleep(5000); // 5秒延迟
syncToMySQL(message);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 死信队列处理(人工介入)
@KafkaListener(topics = "db-sync-dead-letter", groupId = "db-sync-dead-group")
public void handleDeadLetter(String message) {
log.error("Dead letter received: {}", message);
// 发送告警、记录日志、人工处理
alertService.sendAlert("数据同步失败需要人工处理: " + message);
}
}
@Data
class SyncEvent {
private String key;
private Map
private long timestamp;
private int retryCount;
}
2.2 强一致性模型(分布式事务)
适用场景
:金融、订单等对一致性要求极高的场景。
实现方案:Seata分布式事务框架
/**
* 使用Seata实现分布式事务
* 保证Redis和MySQL的强一致性
*/
@Service
public class StrongConsistencyService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
@GlobalTransactional // Seata全局事务注解
public void createOrder(Order order) {
// 1. 扣减库存(MySQL)
int updated = jdbcTemplate.update(
“UPDATE inventory SET stock = stock – ? WHERE product_id = ? AND stock >= ?”,
order.getQuantity(), order.getProductId(), order.getQuantity()
);
if (updated == 0) {
throw new RuntimeException(“库存不足”);
}
// 2. 写入订单(MySQL)
jdbcTemplate.update(
“INSERT INTO orders (id, product_id, quantity, user_id) VALUES (?, ?, ?, ?)”,
order.getId(), order.getProductId(), order.getQuantity(), order.getUserId()
);
// 3. 更新Redis缓存
String inventoryKey = “inventory:” + order.getProductId();
Integer currentStock = (Integer) redisTemplate.opsForHash().get(inventoryKey, “stock”);
if (currentStock != null) {
redisTemplate.opsForHash().put(inventoryKey, “stock”, currentStock – order.getQuantity());
}
// 4. 写入Redis Stream(用于后续同步)
Map
streamData.put(“operation”, “ORDER_CREATE”);
streamData.put(“order_id”, order.getId());
streamData.put(“product_id”, order.getProductId());
streamData.put(“quantity”, order.getQuantity().toString());
redisTemplate.opsForStream().add(“order:stream”, streamData);
// 如果发生异常,Seata会自动回滚所有操作(包括Redis)
// 需要实现Redis的undo log(通过AOP拦截)
}
}
2.3 读写策略与缓存模式
Cache-Aside Pattern(旁路缓存模式)
/**
* Cache-Aside模式实现
* 读取时:先读缓存,缓存未命中读数据库,再写缓存
* 写入时:先删缓存,再写数据库
*/
@Service
public class CacheAsideService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 读取数据(Cache-Aside)
*/
public Product getProduct(Long id) {
String redisKey = “product:” + id;
// 1. 先尝试从Redis读取
Map
if (!cachedData.isEmpty()) {
return convertToProduct(cachedData);
}
// 2. 缓存未命中,从数据库读取
Product product = jdbcTemplate.queryForObject(
“SELECT * FROM product WHERE id = ?”,
(rs, rowNum) -> new Product(
rs.getLong(“id”),
rs.getString(“name”),
rs.getBigDecimal(“price”),
rs.getInt(“stock”)
),
id
);
// 3. 写入Redis缓存
if (product != null) {
Map
dataMap.put(“id”, product.getId());
dataMap.put(“name”, product.getName());
dataMap.put(“price”, product.getPrice());
dataMap.put(“stock”, product.getStock());
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
}
return product;
}
/**
* 写入数据(Cache-Aside)
* 先删缓存,再写数据库(避免脏数据)
*/
@Transactional
public void updateProduct(Product product) {
String redisKey = “product:” + product.getId();
// 1. 删除Redis缓存(先删缓存)
redisTemplate.delete(redisKey);
// 2. 更新数据库
jdbcTemplate.update(
“UPDATE product SET name = ?, price = ?, stock = ? WHERE id = ?”,
product.getName(), product.getPrice(), product.getStock(), product.getId()
);
// 3. 延迟双删(解决主从延迟导致的脏数据)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 延迟500ms
redisTemplate.delete(redisKey); // 再次删除
// 4. 发布变更事件(用于其他节点同步)
redisTemplate.convertAndSend(“product:change”,
“UPDATE:” + product.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
/**
* 写入数据(Write-Through模式)
* 同时写入缓存和数据库
*/
@Transactional
public void updateProductWriteThrough(Product product) {
String redisKey = “product:” + product.getId();
// 1. 写入数据库
jdbcTemplate.update(
“UPDATE product SET name = ?, price = ?, stock = ? WHERE id = ?”,
product.getName(), product.getPrice(), product.getStock(), product.getId()
);
// 2. 写入Redis
Map
dataMap.put(“id”, product.getId());
dataMap.put(“name”, product.getName());
dataMap.put(“price”, product.getPrice());
dataMap.put(“stock”, product.getStock());
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
}
}
2.4 解决缓存穿透、击穿、雪崩
完整防护代码
@Component
public class CacheProtectionService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
// 布隆过滤器(防止缓存穿透)
private BloomFilter
@PostConstruct
public void initBloomFilter() {
// 初始化布隆过滤器,预期插入100万条数据,误判率0.01%
idFilter = BloomFilter.create(
Funnels.longFunnel(),
1000000,
0.0001
);
// 预热布隆过滤器(从数据库加载所有ID)
List
(rs, rowNum) -> rs.getLong(“id”));
ids.forEach(idFilter::put);
}
/**
* 防止缓存穿透:布隆过滤器 + 空值缓存
*/
public Product getProductWithBloom(Long id) {
// 1. 布隆过滤器检查
if (!idFilter.mightContain(id)) {
// 一定不存在,直接返回null
return null;
}
String redisKey = “product:” + id;
// 2. 尝试从Redis读取
Map
if (!cachedData.isEmpty()) {
// 检查是否是空值标记
if (cachedData.containsKey(“null”)) {
return null;
}
return convertToProduct(cachedData);
}
// 3. 缓存未命中,查询数据库
try {
Product product = jdbcTemplate.queryForObject(
“SELECT * FROM product WHERE id = ?”,
(rs, rowNum) -> new Product(
rs.getLong(“id”),
rs.getString(“name”),
rs.getBigDecimal(“price”),
rs.getInt(“stock”)
),
id
);
if (product != null) {
// 写入Redis
Map
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
} else {
// 数据不存在,缓存空值(防止缓存穿透)
Map
redisTemplate.opsForHash().putAll(redisKey, nullMap);
redisTemplate.expire(redisKey, 300, TimeUnit.SECONDS); // 空值较短过期时间
}
return product;
} catch (EmptyResultDataAccessException e) {
// 数据库中不存在,缓存空值
Map
redisTemplate.opsForHash().putAll(redisKey, nullMap);
redisTemplate.expire(redisKey, 300, TimeUnit.SECONDS);
return null;
}
}
/**
* 防止缓存击穿:互斥锁 + 逻辑过期
*/
public Product getProductWithMutex(Long id) {
String redisKey = “product:” + id;
String lockKey = “lock:product:” + id;
// 1. 尝试从Redis读取
Map
if (!cachedData.isEmpty()) {
// 检查逻辑过期时间
Long expireTime = (Long) cachedData.get(“expire_time”);
if (expireTime != null && expireTime > System.currentTimeMillis()) {
return convertToProduct(cachedData);
}
}
// 2. 获取分布式锁
boolean locked = tryLock(lockKey, 30, TimeUnit.SECONDS);
if (!locked) {
// 获取锁失败,等待并重试
try {
Thread.sleep(100);
return getProductWithMutex(id); // 递归重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
try {
// 3. 双重检查(获取锁后再次检查缓存)
cachedData = redisTemplate.opsForHash().entries(redisKey);
if (!cachedData.isEmpty()) {
Long expireTime = (Long) cachedData.get(“expire_time”);
if (expireTime != null && expireTime > System.currentTimeMillis()) {
return convertToProduct(cachedData);
}
}
// 4. 查询数据库
Product product = jdbcTemplate.queryForObject(
“SELECT * FROM product WHERE id = ?”,
(rs, rowNum) -> new Product(
rs.getLong(“id”),
rs.getString(“name”),
rs.getBigDecimal(“price”),
rs.getInt(“stock”)
),
id
);
if (product != null) {
// 5. 写入Redis(设置逻辑过期时间)
Map
dataMap.put(“expire_time”, System.currentTimeMillis() + 3600000); // 1小时后过期
redisTemplate.opsForHash().putAll(redisKey, dataMap);
// 不设置Redis自动过期,由逻辑过期控制
}
return product;
} finally {
// 6. 释放锁
unlock(lockKey);
}
}
/**
* 防止缓存雪崩:随机过期时间 + 预热
*/
public void writeProductWithRandomExpire(Product product) {
String redisKey = “product:” + product.getId();
Map
// 基础过期时间 + 随机值(防止同时过期)
int baseExpire = 3600; // 1小时
int randomExpire = new Random().nextInt(600); // 0-10分钟随机
int finalExpire = baseExpire + randomExpire;
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, finalExpire, TimeUnit.SECONDS);
}
// 分布式锁实现(Redis SETNX)
private boolean tryLock(String lockKey, long timeout, TimeUnit unit) {
long waitTime = unit.toMillis(timeout);
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() – startTime < waitTime) {
// SETNX + 过期时间(防止死锁)
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
"1",
30,
TimeUnit.SECONDS
);
if (Boolean.TRUE.equals(locked)) {
return true;
}
// 等待一段时间后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
private void unlock(String lockKey) {
redisTemplate.delete(lockKey);
}
}
三、高可用性架构设计
3.1 Redis高可用架构
3.1.1 Redis Sentinel(哨兵模式)
架构图
:
Redis Master (写节点)
↓ ↑
Redis Slave1 (读节点) ← Sentinel监控
Redis Slave2 (读节点) ← Sentinel监控
Redis Sentinel (监控节点)
配置示例
:
# redis-sentinel.conf
port 26379
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 60000
Java客户端配置
:
@Configuration
public class RedisSentinelConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("192.168.1.101", 26379)
.sentinel("192.168.1.102", 26379)
.sentinel("192.168.1.103", 26379);
// 读从节点策略
LettucePoolingClientConfiguration poolConfig = LettucePoolingClientConfiguration
.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先读从节点
.build();
return new LettuceConnectionFactory(sentinelConfig, poolConfig);
}
@Bean
public RedisTemplate
RedisTemplate
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
3.1.2 Redis Cluster(集群模式)
架构图
:
Node1 (1-5460) Node2 (5461-10922) Node3 (10923-16383)
↓ ↑ ↓ ↑ ↓ ↑
Replica1 Replica2 Replica3
配置示例
:
# redis-cluster.conf
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
Java客户端配置
:
@Configuration
public class RedisClusterConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration()
.addNode(“192.168.1.101:6379”)
.addNode(“192.168.1.102:6379”)
.addNode(“192.168.1.103:6379”)
.addNode(“192.168.1.104:6379”)
.addNode(“192.168.1.105:6379”)
.addNode(“192.168.1.106:6379”);
// 集群自适应配置
LettuceClientConfiguration clientConfig = LettuceClientConfiguration
.builder()
.commandTimeout(Duration.ofSeconds(5))
.autoRefreshAdaptive(true)
.build();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
}
3.2 数据库高可用架构
3.2.1 MySQL主从复制 + MHA自动切换
架构图
:
MySQL Master (写)
↓ ↑
MySQL Slave1 ← MHA Manager
MySQL Slave2 ← MHA Manager
MySQL Slave3
MHA配置
:
# mha.conf
[server default]
user=root
password=rootpass
repl_user=repl
repl_password=replpass
ping_interval=3
master_binlog_dir=/var/lib/mysql
[server1]
hostname=192.168.1.101
candidate_master=1
[server2]
hostname=192.168.1.102
candidate_master=1
[server3]
hostname=192.168.1.103
no_master=1
3.2.2 MySQL Group Replication(组复制)
配置示例
:
— 安装组复制插件
INSTALL PLUGIN group_replication SONAME ‘group_replication.so’;
— 配置组复制
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
— 查看组状态
SELECT * FROM performance_schema.replication_group_members;
3.3 双向同步的高可用方案
3.3.1 多活架构设计
架构图
:
数据中心A:
Redis Master ←→ MySQL Master
↓ ↑ ↓ ↑
Redis Slave MySQL Slave
↓ ↑ ↓ ↑
Canal同步 Canal同步
↓ ↑ ↓ ↑
消息队列 ←→ 消息队列
↓ ↑ ↓ ↑
数据中心B:
Redis Master ←→ MySQL Master
↓ ↑ ↓ ↑
Redis Slave MySQL Slave
代码实现:跨数据中心同步
/**
* 跨数据中心同步服务
* 支持双向同步,避免循环复制
*/
@Service
public class CrossDCSyncService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
// 本地数据中心标识
@Value(“${datacenter.id}”)
private String dcId;
/**
* 写入数据并标记数据中心
*/
public void writeWithDCMark(String key, Map
// 标记数据来源
data.put(“_dc”, dcId);
data.put(“_timestamp”, System.currentTimeMillis());
// 写入Redis
redisTemplate.opsForHash().putAll(key, data);
redisTemplate.expire(key, 3600, TimeUnit.SECONDS);
// 发布到跨DC消息队列
String message = JSON.toJSONString(data);
redisTemplate.convertAndSend(“cross-dc:sync”, message);
}
/**
* 接收跨DC同步消息
*/
@RedisListener
public void handleCrossDCMessage(String message) {
Map
// 检查是否是本DC产生的数据(避免循环)
String sourceDC = (String) data.get(“_dc”);
if (dcId.equals(sourceDC)) {
return; // 忽略本DC数据
}
// 检查时间戳(避免旧数据覆盖新数据)
Long timestamp = (Long) data.get(“_timestamp”);
if (timestamp == null || timestamp < System.currentTimeMillis() - 60000) {
log.warn("Ignoring old data from DC: {}", sourceDC);
return;
}
// 同步到本地MySQL
String key = (String) data.get("key");
if (key != null) {
syncHashToMySQL(key, data);
}
}
/**
* 数据库变更同步到跨DC
*/
@CanalEventListener
public void handleDBChange(CanalEntry.Event event) {
// 只处理本DC的变更
if (!isLocalChange(event)) {
return;
}
// 转换为跨DC消息
for (CanalEntry.RowData rowData : event.getRowDataList()) {
Map
data.put(“_dc”, dcId);
data.put(“_timestamp”, System.currentTimeMillis());
// 发送到跨DC队列
String message = JSON.toJSONString(data);
redisTemplate.convertAndSend(“cross-dc:sync”, message);
}
}
private boolean isLocalChange(CanalEntry.Event event) {
// 通过检查binlog位置或事务ID判断是否为本地变更
// 实际实现需要更复杂的逻辑
return true;
}
}
3.4 监控与告警体系
3.4.1 完整监控代码实现
/**
* 同步监控服务
* 实时监控同步状态和延迟
*/
@Component
public class SyncMonitorService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
// 同步延迟监控(Redis → MySQL)
@Scheduled(fixedRate = 5000) // 每5秒检查一次
public void monitorSyncDelay() {
// 获取Redis中最新数据的时间戳
Set
if (keys == null) return;
for (String key : keys) {
Long redisTimestamp = (Long) redisTemplate.opsForHash().get(key, “_timestamp”);
if (redisTimestamp == null) continue;
// 获取MySQL中对应数据的时间戳
String[] parts = key.split(“:”);
if (parts.length < 2) continue;
String tableName = parts[0];
String id = parts[1];
try {
Long mysqlTimestamp = jdbcTemplate.queryForObject(
"SELECT _timestamp FROM " + tableName + " WHERE id = ?",
Long.class, id
);
if (mysqlTimestamp != null) {
long delay = redisTimestamp - mysqlTimestamp;
// 记录延迟指标
recordMetric("sync.delay." + tableName, delay);
// 告警:延迟超过10秒
if (delay > 10000) {
sendAlert(“同步延迟过高: ” + key + “, delay=” + delay + “ms”);
}
}
} catch (Exception e) {
// 数据不存在或查询失败
recordMetric(“sync.missing.” + tableName, 1);
}
}
}
// 同步失败率监控
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void monitorSyncFailureRate() {
// 检查死信队列大小
Long deadLetterSize = redisTemplate.opsForList().size(“sync:failed”);
if (deadLetterSize != null && deadLetterSize > 100) {
sendAlert(“同步失败积压严重: ” + deadLetterSize + “条”);
}
// 检查最近5分钟的失败率
long fiveMinutesAgo = System.currentTimeMillis() – 300000;
Long failureCount = redisTemplate.opsForZSet().count(
“sync:failure:log”,
fiveMinutesAgo,
System.currentTimeMillis()
);
if (failureCount != null && failureCount > 10) {
sendAlert(“5分钟内同步失败次数: ” + failureCount);
}
}
// Redis和MySQL连接健康检查
@Scheduled(fixedRate = 10000)
public void healthCheck() {
// Redis健康检查
try {
redisTemplate.getConnectionFactory().getConnection().ping();
recordMetric(“health.redis”, 1);
} catch (Exception e) {
recordMetric(“health.redis”, 0);
sendAlert(“Redis连接异常: ” + e.getMessage());
}
// MySQL健康检查
try {
jdbcTemplate.execute(“SELECT 1”);
recordMetric(“health.mysql”, 1);
} catch (Exception e) {
recordMetric(“health.mysql”, 0);
sendAlert(“MySQL连接异常: ” + e.getMessage());
}
}
// 记录监控指标(可接入Prometheus)
private void recordMetric(String metric, double value) {
// 写入Redis时序数据结构(RedisTimeSeries或普通key)
String key = “metric:” + metric + “:” + System.currentTimeMillis();
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
// 或者推送到Prometheus Pushgateway
// pushToPrometheus(metric, value);
}
private void sendAlert(String message) {
// 发送告警到钉钉、企业微信、邮件等
log.error(“ALERT: {}”, message);
// 实际实现调用告警服务
}
}
四、完整生产环境配置示例
4.1 Docker Compose部署配置
version: ‘3.8’
services:
# MySQL主从配置
mysql-master:
image: mysql:8.0
container_name: mysql-master
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: appdb
ports:
– “3306:3306”
volumes:
– ./mysql/master/my.cnf:/etc/mysql/my.cnf
– ./mysql/master/data:/var/lib/mysql
networks:
– sync-network
mysql-slave1:
image: mysql:8.0
container_name: mysql-slave1
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: appdb
ports:
– “3307:3306”
volumes:
– ./mysql/slave1/my.cnf:/etc/mysql/my.cnf
– ./mysql/slave1/data:/var/lib/mysql
depends_on:
– mysql-master
networks:
– sync-network
# Redis Sentinel配置
redis-master:
image: redis:7-alpine
container_name: redis-master
command: redis-server /etc/redis/redis.conf
ports:
– “6379:6379”
volumes:
– ./redis/master/redis.conf:/etc/redis/redis.conf
networks:
– sync-network
redis-slave1:
image: redis:7-alpine
container_name: redis-slave1
command: redis-server /etc/redis/redis.conf
ports:
– “6380:6379”
volumes:
– ./redis/slave1/redis.conf:/etc/redis/redis.conf
depends_on:
– redis-master
networks:
– sync-network
redis-sentinel:
image: redis:7-alpine
container_name: redis-sentinel
command: redis-server /etc/redis/sentinel.conf
ports:
– “26379:26379”
volumes:
– ./redis/sentinel/sentinel.conf:/etc/redis/sentinel.conf
depends_on:
– redis-master
networks:
– sync-network
# Canal同步服务
canal-server:
image: canal/canal-server:latest
container_name: canal-server
environment:
canal.instance.master.address: mysql-master:3306
canal.instance.dbUsername: canal
canal.instance.dbPassword: canal
canal.instance.filter.regex: appdb\\..*
ports:
– “11111:11111”
depends_on:
– mysql-master
networks:
– sync-network
# 应用服务(Spring Boot)
app-service:
build: ./app
container_name: app-service
environment:
SPRING_REDIS_SENTINEL_NODES: redis-sentinel:26379
SPRING_DATASOURCE_URL: jdbc:mysql://mysql-master:3306/appdb
ports:
– “8080:8080”
depends_on:
– mysql-master
– redis-master
– canal-server
networks:
– sync-network
networks:
sync-network:
driver: bridge
4.2 Redis配置文件示例
redis.conf (Master)
# 基础配置
bind 0.0.0.0
port 6379
daemonize no
pidfile /var/run/redis_6379.pid
logfile /var/log/redis/redis.log
# 持久化配置
save 900 1
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
dir /data
# AOF配置
appendonly yes
appendfilename “appendonly.aof”
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 内存配置
maxmemory 2gb
maxmemory-policy allkeys-lru
# 主从配置
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-backlog-size 1gb
repl-timeout 60
# Sentinel相关
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 60000
sentinel.conf
port 26379
daemonize no
logfile /var/log/redis/sentinel.log
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 60000
sentinel auth-pass mymaster rootpass
sentinel announce-ip redis-sentinel
sentinel announce-port 26379
4.3 MySQL主从配置
Master my.cnf
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW
expire_logs_days=7
# 同步专用账号
binlog_do_db=appdb
# 性能优化
innodb_flush_log_at_trx_commit=1
sync_binlog=1
Slave my.cnf
[mysqld]
server-id=2
relay-log=mysql-relay-bin
log-slave-updates=1
read-only=1
# 复制过滤
replicate-do-db=appdb
五、最佳实践与性能优化
5.1 性能优化策略
5.1.1 批量操作优化
/**
* 批量同步优化:使用Pipeline减少网络往返
*/
@Service
public class BatchSyncService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 批量从MySQL同步到Redis
*/
public void batchSyncFromMySQL(List
if (ids.isEmpty()) return;
// 1. 批量查询MySQL
String sql = “SELECT * FROM product WHERE id IN (” +
String.join(“,”, Collections.nCopies(ids.size(), “?”)) + “)”;
List
(rs, rowNum) -> new Product(
rs.getLong(“id”),
rs.getString(“name”),
rs.getBigDecimal(“price”),
rs.getInt(“stock”)
),
ids.toArray()
);
// 2. 使用Pipeline批量写入Redis
redisTemplate.executePipelined((RedisCallback
for (Product product : products) {
String key = “product:” + product.getId();
Map
// 使用原始连接避免序列化开销
byte[] keyBytes = key.getBytes();
for (Map.Entry
connection.hSet(keyBytes,
entry.getKey().getBytes(),
String.valueOf(entry.getValue()).getBytes());
}
// 设置过期时间
connection.expire(keyBytes, 3600);
}
return null;
});
}
/**
* 批量从Redis同步到MySQL
*/
public void batchSyncToMySQL(List
if (keys.isEmpty()) return;
// 1. 使用Pipeline批量读取Redis
List
// 2. 批量写入MySQL(使用JDBC Batch)
String sql = “INSERT INTO product (id, name, price, stock) VALUES (?, ?, ?, ?) ” +
“ON DUPLICATE KEY UPDATE name=?, price=?, stock=?”;
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Map
ps.setLong(1, Long.parseLong(data.get(“id”).toString()));
ps.setString(2, data.get(“name”).toString());
ps.setBigDecimal(3, new BigDecimal(data.get(“price”).toString()));
ps.setInt(4, Integer.parseInt(data.get(“stock”).toString()));
// 更新部分
ps.setString(5, data.get(“name”).toString());
ps.setBigDecimal(6, new BigDecimal(data.get(“price”).toString()));
ps.setInt(7, Integer.parseInt(data.get(“stock”).toString()));
}
@Override
public int getBatchSize() {
return dataList.size();
}
});
}
}
5.1.2 异步处理优化
/**
* 异步同步服务:使用线程池和队列
*/
@Service
public class AsyncSyncService {
private final ExecutorService syncExecutor = new ThreadPoolExecutor(
4, 8, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat(“sync-worker-%d”).build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时主线程执行
);
private final BlockingQueue
@PostConstruct
public void init() {
// 启动消费者线程
for (int i = 0; i < 4; i++) {
syncExecutor.submit(this::processSyncTask);
}
}
/**
* 异步提交同步任务
*/
public CompletableFuture
return CompletableFuture.runAsync(() -> {
try {
taskQueue.put(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, syncExecutor);
}
/**
* 任务处理线程
*/
private void processSyncTask() {
while (!Thread.currentThread().isInterrupted()) {
try {
SyncTask task = taskQueue.poll(1, TimeUnit.SECONDS);
if (task != null) {
executeTask(task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error(“Task execution failed”, e);
// 失败重试
if (task.getRetryCount() < 3) {
task.setRetryCount(task.getRetryCount() + 1);
try {
taskQueue.put(task);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
}
private void executeTask(SyncTask task) {
switch (task.getType()) {
case REDIS_TO_MYSQL:
syncHashToMySQL(task.getKey(), task.getData());
break;
case MYSQL_TO_REDIS:
// 从MySQL查询最新数据并写入Redis
break;
case DELETE:
redisTemplate.delete(task.getKey());
break;
}
}
}
@Data
class SyncTask {
private String key;
private Map
private SyncType type;
private int retryCount;
private long timestamp;
}
enum SyncType {
REDIS_TO_MYSQL,
MYSQL_TO_REDIS,
DELETE
}
5.2 数据一致性校验
/**
* 数据一致性校验服务
* 定期检查Redis和MySQL数据是否一致
*/
@Component
public class ConsistencyCheckService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
// 每天凌晨2点执行全量校验
@Scheduled(cron = “0 0 2 * * ?”)
public void fullConsistencyCheck() {
log.info(“Starting full consistency check…”);
// 1. 获取所有Redis Key
Set
if (redisKeys == null) return;
// 2. 分批校验(避免内存溢出)
int batchSize = 1000;
List
for (int i = 0; i < keyList.size(); i += batchSize) {
int end = Math.min(i + batchSize, keyList.size());
List
// 并行校验
batch.parallelStream().forEach(this::checkSingleKey);
log.info(“Checked batch {}/{}”, end, keyList.size());
}
log.info(“Full consistency check completed”);
}
/**
* 校验单个Key的一致性
*/
private void checkSingleKey(String redisKey) {
try {
// 从Redis读取
Map
if (redisData.isEmpty()) return;
// 解析ID
String[] parts = redisKey.split(“:”);
if (parts.length < 2) return;
String tableName = parts[0];
String id = parts[1];
// 从MySQL读取
Map
“SELECT * FROM ” + tableName + ” WHERE id = ?”, id
);
// 比较数据
if (!isDataEqual(redisData, mysqlData)) {
log.warn(“Data inconsistency detected for key: {}”, redisKey);
log.warn(“Redis: {}”, redisData);
log.warn(“MySQL: {}”, mysqlData);
// 自动修复(可选)
autoFixInconsistency(redisKey, tableName, id, redisData, mysqlData);
}
} catch (Exception e) {
log.error(“Error checking key: {}”, redisKey, e);
}
}
/**
* 自动修复不一致数据
*/
private void autoFixInconsistency(String redisKey, String tableName, String id,
Map
// 策略:以MySQL为准(持久化优先)
if (mysqlData.isEmpty()) {
// MySQL无数据,删除Redis
redisTemplate.delete(redisKey);
log.info(“Auto fix: deleted Redis key {}”, redisKey);
} else {
// MySQL有数据,更新Redis
Map
mysqlData.forEach((k, v) -> dataMap.put(k, v));
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
log.info(“Auto fix: updated Redis key {}”, redisKey);
}
}
/**
* 数据比较
*/
private boolean isDataEqual(Map
if (redisData.size() != mysqlData.size()) {
return false;
}
for (Map.Entry
String key = entry.getKey().toString();
Object redisValue = entry.getValue();
Object mysqlValue = mysqlData.get(key);
if (mysqlValue == null) {
return false;
}
// 类型转换后比较
if (!String.valueOf(redisValue).equals(String.valueOf(mysqlValue))) {
return false;
}
}
return true;
}
/**
* 增量校验(基于时间戳)
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void incrementalCheck() {
long fiveMinutesAgo = System.currentTimeMillis() – 300000;
// 检查最近5分钟变更的数据
Set
“sync:recent”,
fiveMinutesAgo,
System.currentTimeMillis()
);
if (keys != null) {
for (String key : keys) {
checkSingleKey(key);
}
}
}
}
5.3 容灾与故障恢复
/**
* 容灾恢复服务
*/
@Service
public class DisasterRecoveryService {
@Autowired
private RedisTemplate
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* Redis宕机后从MySQL恢复缓存
*/
public void restoreRedisFromMySQL() {
log.info(“Starting Redis restoration from MySQL…”);
// 1. 清空Redis(避免脏数据)
redisTemplate.execute((RedisCallback
connection.flushDB();
return null;
});
// 2. 分批从MySQL加载数据
int pageSize = 1000;
int page = 0;
while (true) {
List
“SELECT * FROM product LIMIT ? OFFSET ?”,
(rs, rowNum) -> new Product(
rs.getLong(“id”),
rs.getString(“name”),
rs.getBigDecimal(“price”),
rs.getInt(“stock”)
),
pageSize,
page * pageSize
);
if (products.isEmpty()) break;
// 批量写入Redis
batchWriteToRedis(products);
log.info(“Restored {} products”, (page + 1) * pageSize);
page++;
}
log.info(“Redis restoration completed”);
}
/**
* MySQL宕机后从Redis恢复数据
*/
public void restoreMySQLFromRedis() {
log.info(“Starting MySQL restoration from Redis…”);
// 1. 获取所有Redis数据
Set
if (keys == null || keys.isEmpty()) {
log.warn(“No data in Redis for restoration”);
return;
}
// 2. 分批恢复到MySQL
List
int batchSize = 500;
for (int i = 0; i < keyList.size(); i += batchSize) {
int end = Math.min(i + batchSize, keyList.size());
List
// 批量读取Redis
List
for (String key : batch) {
Map
if (!data.isEmpty()) {
Map
data.forEach((k, v) -> map.put(k.toString(), v));
dataList.add(map);
}
}
// 批量写入MySQL
batchInsertToMySQL(dataList);
log.info(“Restored {} records”, end);
}
log.info(“MySQL restoration completed”);
}
private void batchWriteToRedis(List
redisTemplate.executePipelined((RedisCallback
for (Product product : products) {
String key = “product:” + product.getId();
Map
byte[] keyBytes = key.getBytes();
for (Map.Entry
connection.hSet(keyBytes,
entry.getKey().getBytes(),
String.valueOf(entry.getValue()).getBytes());
}
connection.expire(keyBytes, 3600);
}
return null;
});
}
private void batchInsertToMySQL(List
String sql = “INSERT INTO product (id, name, price, stock) VALUES (?, ?, ?, ?) ” +
“ON DUPLICATE KEY UPDATE name=?, price=?, stock=?”;
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Map
ps.setLong(1, Long.parseLong(data.get(“id”).toString()));
ps.setString(2, data.get(“name”).toString());
ps.setBigDecimal(3, new BigDecimal(data.get(“price”).toString()));
ps.setInt(4, Integer.parseInt(data.get(“stock”).toString()));
ps.setString(5, data.get(“name”).toString());
ps.setBigDecimal(6, new BigDecimal(data.get(“price”).toString()));
ps.setInt(7, Integer.parseInt(data.get(“stock”).toString()));
}
@Override
public int getBatchSize() {
return dataList.size();
}
});
}
}
六、总结与建议
6.1 方案选择指南
场景
推荐方案
理由
高并发读多写少
Cache-Aside + Redis Sentinel
简单可靠,读性能最优
强一致性要求
Seata分布式事务 + 双写
保证数据绝对一致
最终一致性可接受
Canal + 消息队列
性能最好,异步解耦
跨数据中心
Redis Stream + DC标记
避免循环复制,支持多活
金融级场景
TCC模式 + 人工审核
最高可靠性,可追溯
6.2 关键要点总结
数据一致性
:
优先保证MySQL持久化
使用最终一致性模型
实现补偿机制和重试策略
高可用性
:
Redis Sentinel/Cluster
MySQL主从 + MHA
多活架构 + 健康检查
性能优化
:
Pipeline批量操作
异步处理
合理的缓存策略
监控告警
:
实时监控同步延迟
自动修复不一致数据
完善的日志和告警
容灾恢复
:
定期备份
自动恢复机制
人工介入流程
通过以上方案的组合使用,可以构建一个既保证数据一致性又具备高可用性的Redis与数据库双向同步系统。实际部署时需要根据业务特点、数据规模和一致性要求选择合适的方案组合。