Redis与数据库双向同步机制如何实现数据一致性与高可用性挑战

引言:理解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 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 rowDatasList = event.getRowDataList();
for (CanalEntry.RowData rowData : rowDatasList) {
// 构建Redis键值对
String primaryKey = getPrimaryKey(rowData);
Map dataMap = convertRowDataToMap(rowData);

// 写入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 rowDatasList = event.getRowDataList();
for (CanalEntry.RowData rowData : rowDatasList) {
String primaryKey = getPrimaryKey(rowData);
Map newDataMap = convertRowDataToMap(rowData);

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 rowDatasList = event.getRowDataList();
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 convertRowDataToMap(CanalEntry.RowData rowData) {
Map dataMap = new HashMap<>();
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 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 keys = redisTemplate.keys(“*”);
if (keys == null || keys.isEmpty()) return;

List pipelineResults = redisTemplate.executePipelined((RedisCallback) connection -> {
for (String key : keys) {
// 检查Key是否标记为需要同步
if (connection.exists(key.getBytes())) {
byte[] syncFlag = connection.get((“sync:” + key).getBytes());
if (syncFlag != null && “1”.equals(new String(syncFlag))) {
// 获取数据并同步到MySQL
Map data = redisTemplate.opsForHash().entries(key);
syncHashToMySQL(key, data);

// 清除同步标记
connection.del((“sync:” + key).getBytes());
}
}
}
return null;
});
}

// 手动触发同步(在写入Redis时调用)
public void writeWithSync(String key, Map data) {
// 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 data) {
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 params = new ArrayList<>();
List columns = new ArrayList<>();

for (Map.Entry entry : data.entrySet()) {
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 data = redisTemplate.opsForHash().entries(redisKey);
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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

@Autowired
private KafkaTemplate kafkaTemplate; // 也可用Redis Pub/Sub

/**
* 双写模式:同时写入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 = new HashMap<>();
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 dataMap = convertToMap(product);
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);

// 2. 写入Redis Stream(保证消息不丢失)
Map streamData = new HashMap<>();
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> messages =
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 message : messages) {
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 message) {
Map data = message.getValue();
String operation = data.get(“operation”);
String table = data.get(“table”);
String jsonData = data.get(“data”);

// 解析并同步到MySQL
Map rowData = JSON.parseObject(jsonData);
syncHashToMySQL(table + “:” + rowData.get(“id”), rowData);
}
}
二、数据一致性保障机制
2.1 最终一致性模型
核心思想
:允许短暂的不一致,通过补偿机制最终达到一致。
实现方案:基于消息队列的异步同步
/**
* 最终一致性保障:使用消息队列解耦
* 1. 写入Redis成功后,发送消息到MQ
* 2. 消费者从MQ读取消息,同步到MySQL
* 3. 失败重试 + 死信队列
*/
@Service
public class EventualConsistencyService {

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private KafkaTemplate kafkaTemplate;

// 写入数据(保证Redis成功即可返回)
public void writeData(String key, Map data) {
// 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 data;
private long timestamp;
private int retryCount;
}
2.2 强一致性模型(分布式事务)
适用场景
:金融、订单等对一致性要求极高的场景。
实现方案:Seata分布式事务框架
/**
* 使用Seata实现分布式事务
* 保证Redis和MySQL的强一致性
*/
@Service
public class StrongConsistencyService {

@Autowired
private RedisTemplate 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 = new HashMap<>();
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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

/**
* 读取数据(Cache-Aside)
*/
public Product getProduct(Long id) {
String redisKey = “product:” + id;

// 1. 先尝试从Redis读取
Map cachedData = redisTemplate.opsForHash().entries(redisKey);
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 = new HashMap<>();
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 = new HashMap<>();
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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

// 布隆过滤器(防止缓存穿透)
private BloomFilter idFilter;

@PostConstruct
public void initBloomFilter() {
// 初始化布隆过滤器,预期插入100万条数据,误判率0.01%
idFilter = BloomFilter.create(
Funnels.longFunnel(),
1000000,
0.0001
);

// 预热布隆过滤器(从数据库加载所有ID)
List ids = jdbcTemplate.query(“SELECT id FROM product”,
(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 cachedData = redisTemplate.opsForHash().entries(redisKey);
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 dataMap = convertToMap(product);
redisTemplate.opsForHash().putAll(redisKey, dataMap);
redisTemplate.expire(redisKey, 3600, TimeUnit.SECONDS);
} else {
// 数据不存在,缓存空值(防止缓存穿透)
Map nullMap = Collections.singletonMap(“null”, “true”);
redisTemplate.opsForHash().putAll(redisKey, nullMap);
redisTemplate.expire(redisKey, 300, TimeUnit.SECONDS); // 空值较短过期时间
}

return product;

} catch (EmptyResultDataAccessException e) {
// 数据库中不存在,缓存空值
Map nullMap = Collections.singletonMap(“null”, “true”);
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 cachedData = redisTemplate.opsForHash().entries(redisKey);
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 = convertToMap(product);
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 dataMap = convertToMap(product);

// 基础过期时间 + 随机值(防止同时过期)
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(RedisConnectionFactory factory) {
RedisTemplate template = new 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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

// 本地数据中心标识
@Value(“${datacenter.id}”)
private String dcId;

/**
* 写入数据并标记数据中心
*/
public void writeWithDCMark(String key, Map data) {
// 标记数据来源
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 data = JSON.parseObject(message);

// 检查是否是本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 = convertRowDataToMap(rowData);
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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

// 同步延迟监控(Redis → MySQL)
@Scheduled(fixedRate = 5000) // 每5秒检查一次
public void monitorSyncDelay() {
// 获取Redis中最新数据的时间戳
Set keys = redisTemplate.keys(“product:*”);
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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

/**
* 批量从MySQL同步到Redis
*/
public void batchSyncFromMySQL(List ids) {
if (ids.isEmpty()) return;

// 1. 批量查询MySQL
String sql = “SELECT * FROM product WHERE id IN (” +
String.join(“,”, Collections.nCopies(ids.size(), “?”)) + “)”;

List products = jdbcTemplate.query(sql,
(rs, rowNum) -> new Product(
rs.getLong(“id”),
rs.getString(“name”),
rs.getBigDecimal(“price”),
rs.getInt(“stock”)
),
ids.toArray()
);

// 2. 使用Pipeline批量写入Redis
redisTemplate.executePipelined((RedisCallback) connection -> {
for (Product product : products) {
String key = “product:” + product.getId();
Map data = convertToMap(product);

// 使用原始连接避免序列化开销
byte[] keyBytes = key.getBytes();
for (Map.Entry entry : data.entrySet()) {
connection.hSet(keyBytes,
entry.getKey().getBytes(),
String.valueOf(entry.getValue()).getBytes());
}

// 设置过期时间
connection.expire(keyBytes, 3600);
}
return null;
});
}

/**
* 批量从Redis同步到MySQL
*/
public void batchSyncToMySQL(List keys) {
if (keys.isEmpty()) return;

// 1. 使用Pipeline批量读取Redis
List> dataList = redisTemplate.executePipelined(
(RedisCallback>) connection -> {
List> results = new ArrayList<>();
for (String key : keys) {
Map data = connection.hGetAll(key.getBytes());
Map map = new HashMap<>();
for (Map.Entry entry : data.entrySet()) {
map.put(new String(entry.getKey()), new String(entry.getValue()));
}
results.add(map);
}
return results;
}
);

// 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 data = dataList.get(i);
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 taskQueue = new LinkedBlockingQueue<>(10000);

@PostConstruct
public void init() {
// 启动消费者线程
for (int i = 0; i < 4; i++) { syncExecutor.submit(this::processSyncTask); } } /** * 异步提交同步任务 */ public CompletableFuture submitSyncTask(SyncTask task) {
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 data;
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 redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

// 每天凌晨2点执行全量校验
@Scheduled(cron = “0 0 2 * * ?”)
public void fullConsistencyCheck() {
log.info(“Starting full consistency check…”);

// 1. 获取所有Redis Key
Set redisKeys = redisTemplate.keys(“product:*”);
if (redisKeys == null) return;

// 2. 分批校验(避免内存溢出)
int batchSize = 1000;
List keyList = new ArrayList<>(redisKeys);

for (int i = 0; i < keyList.size(); i += batchSize) { int end = Math.min(i + batchSize, keyList.size()); List batch = keyList.subList(i, end);

// 并行校验
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 redisData = redisTemplate.opsForHash().entries(redisKey);
if (redisData.isEmpty()) return;

// 解析ID
String[] parts = redisKey.split(“:”);
if (parts.length < 2) return; String tableName = parts[0]; String id = parts[1]; // 从MySQL读取 Map mysqlData = jdbcTemplate.queryForMap(
“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 redisData, Map mysqlData) {
// 策略:以MySQL为准(持久化优先)
if (mysqlData.isEmpty()) {
// MySQL无数据,删除Redis
redisTemplate.delete(redisKey);
log.info(“Auto fix: deleted Redis key {}”, redisKey);
} else {
// MySQL有数据,更新Redis
Map dataMap = new HashMap<>();
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 redisData, Map mysqlData) {
if (redisData.size() != mysqlData.size()) {
return false;
}

for (Map.Entry entry : redisData.entrySet()) {
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 keys = redisTemplate.opsForZSet().rangeByScore(
“sync:recent”,
fiveMinutesAgo,
System.currentTimeMillis()
);

if (keys != null) {
for (String key : keys) {
checkSingleKey(key);
}
}
}
}
5.3 容灾与故障恢复
/**
* 容灾恢复服务
*/
@Service
public class DisasterRecoveryService {

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

/**
* Redis宕机后从MySQL恢复缓存
*/
public void restoreRedisFromMySQL() {
log.info(“Starting Redis restoration from MySQL…”);

// 1. 清空Redis(避免脏数据)
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDB();
return null;
});

// 2. 分批从MySQL加载数据
int pageSize = 1000;
int page = 0;

while (true) {
List products = jdbcTemplate.query(
“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 keys = redisTemplate.keys(“product:*”);
if (keys == null || keys.isEmpty()) {
log.warn(“No data in Redis for restoration”);
return;
}

// 2. 分批恢复到MySQL
List keyList = new ArrayList<>(keys);
int batchSize = 500;

for (int i = 0; i < keyList.size(); i += batchSize) { int end = Math.min(i + batchSize, keyList.size()); List batch = keyList.subList(i, end);

// 批量读取Redis
List> dataList = new ArrayList<>();
for (String key : batch) {
Map data = redisTemplate.opsForHash().entries(key);
if (!data.isEmpty()) {
Map map = new HashMap<>();
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 products) {
redisTemplate.executePipelined((RedisCallback) connection -> {
for (Product product : products) {
String key = “product:” + product.getId();
Map data = convertToMap(product);

byte[] keyBytes = key.getBytes();
for (Map.Entry entry : data.entrySet()) {
connection.hSet(keyBytes,
entry.getKey().getBytes(),
String.valueOf(entry.getValue()).getBytes());
}
connection.expire(keyBytes, 3600);
}
return null;
});
}

private void batchInsertToMySQL(List> dataList) {
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 data = dataList.get(i);
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与数据库双向同步系统。实际部署时需要根据业务特点、数据规模和一致性要求选择合适的方案组合。