企业信息化管理系统

EIMS - 助力企业数字化转型

企业信息化系统实时数据同步与双写架构设计

引言

在企业信息化系统的高可用架构设计中,跨机房数据同步与双写是保障业务连续性的关键技术。本文将探讨如何设计一套可靠的实时数据同步方案,确保在多活架构下数据的一致性与可用性。

双写架构概述

双写架构是指同时向多个数据源写入数据,确保当主数据源发生故障时,业务可以快速切换到备用数据源,保障服务的持续可用。

组件 职责 选型建议
写入代理 拦截并分发写入请求 应用层双写/中间件
消息队列 异步传输与削峰填谷 Kafka/RocketMQ
同步服务 执行具体同步逻辑 Canal/Debezium
冲突解决 处理并发写入冲突 Last-Write-Wins/业务自定义

核心代码实现

双写代理核心实现:

// 双写管理器
class DualWriteManager {
  constructor(config) {
    this.sources = config.sources; // 数据源列表
    this.syncMode = config.syncMode || 'async'; // 同步模式
    this.conflictStrategy = config.conflictStrategy || 'last-write-wins';
    this.retryConfig = config.retry || { maxRetries: 3, backoff: 1000 };
    this.metrics = {
      totalWrites: 0,
      successfulWrites: 0,
      failedWrites: 0,
      conflictCount: 0
    };
  }

  // 写入操作
  async write(table, data, operation = 'insert') {
    this.metrics.totalWrites++;

    const writeTasks = this.sources.map(source =>
      this.writeToSource(source, table, data, operation)
    );

    try {
      // 并行写入所有数据源
      const results = await Promise.allSettled(writeTasks);

      // 分析写入结果
      const successResults = results.filter(r => r.status === 'fulfilled');
      const failedResults = results.filter(r => r.status === 'rejected');

      if (successResults.length === 0) {
        throw new Error('All sources write failed');
      }

      // 记录失败源用于后续补偿
      const failedSources = failedResults.map((r, i) => ({
        source: this.sources[i],
        error: r.reason
      }));

      if (failedSources.length > 0) {
        // 触发补偿任务
        await this.scheduleCompensation(failedSources, table, data, operation);
      }

      this.metrics.successfulWrites += successResults.length;
      this.metrics.failedWrites += failedSources.length;

      return {
        success: true,
        sourcesWritten: successResults.length,
        failedSources
      };

    } catch (error) {
      this.metrics.failedWrites++;
      throw error;
    }
  }

  // 写入单个数据源
  async writeToSource(source, table, data, operation) {
    const retryConfig = this.retryConfig;
    let lastError;

    for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
      try {
        const connection = await this.getConnection(source);

        switch (operation) {
          case 'insert':
            await connection.insert(table, data);
            break;
          case 'update':
            await connection.update(table, data.where, data.values);
            break;
          case 'delete':
            await connection.delete(table, data.where);
            break;
        }

        await this.releaseConnection(source, connection);
        return { source: source.name, success: true };

      } catch (error) {
        lastError = error;

        if (attempt < retryConfig.maxRetries) {
          // 指数退避重试
          await this.sleep(retryConfig.backoff * Math.pow(2, attempt));
        }
      }
    }

    throw lastError;
  }

  // 批量写入优化
  async batchWrite(table, records, operation = 'insert') {
    const batchSize = 100;
    const batches = [];

    for (let i = 0; i < records.length; i += batchSize) {
      batches.push(records.slice(i, i + batchSize));
    }

    const results = [];
    for (const batch of batches) {
      const batchResult = await this.writeBatchToSources(table, batch, operation);
      results.push(batchResult);
    }

    return this.aggregateResults(results);
  }

  // 批量写入到所有数据源
  async writeBatchToSources(table, records, operation) {
    const tasks = this.sources.map(source =>
      this.writeBatchToSingleSource(source, table, records, operation)
    );

    const results = await Promise.allSettled(tasks);
    return results;
  }

  // 获取数据库连接
  async getConnection(source) {
    if (!source.pool) {
      source.pool = await createConnectionPool(source.config);
    }
    return await source.pool.acquire();
  }

  // 释放数据库连接
  async releaseConnection(source, connection) {
    await source.pool.release(connection);
  }

  // 延迟函数
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 获取监控指标
  getMetrics() {
    return {
      ...this.metrics,
      successRate: this.metrics.totalWrites > 0
        ? (this.metrics.successfulWrites / this.metrics.totalWrites * 100).toFixed(2) + '%'
        : '0%'
    };
  }
}

// 数据同步服务
class SyncService {
  constructor(manager) {
    this.manager = manager;
    this.syncQueues = new Map();
    this.listeners = [];
  }

  // 监听数据变更
  async listenToChanges(sourceConfig) {
    const debezium = new DebeziumConnector({
      host: sourceConfig.host,
      port: sourceConfig.port,
      database: sourceConfig.database,
      tables: sourceConfig.tables,
      topic: sourceConfig.topic
    });

    await debezium.connect();

    debezium.on('change', async (change) => {
      await this.handleChange(change);
    });

    return debezium;
  }

  // 处理数据变更
  async handleChange(change) {
    const { operation, before, after, table, timestamp } = change;

    let op, data;
    switch (operation) {
      case 'c': // create
        op = 'insert';
        data = after;
        break;
      case 'u': // update
        op = 'update';
        data = { where: { id: before.id }, values: after };
        break;
      case 'd': // delete
        op = 'delete';
        data = { where: { id: before.id } };
        break;
      default:
        return;
    }

    // 执行双写
    try {
      await this.manager.write(table, data, op);

      // 通知监听器
      this.listeners.forEach(listener => {
        listener({ table, operation: op, data, timestamp });
      });
    } catch (error) {
      console.error('Sync failed:', error);
      // 写入重试队列
      await this.addToRetryQueue({ table, operation: op, data, timestamp });
    }
  }

  // 添加到重试队列
  async addToRetryQueue(item) {
    const queueKey = `${item.table}_${Date.now()}`;
    this.syncQueues.set(queueKey, {
      ...item,
      retryCount: 0,
      nextRetryTime: Date.now()
    });
  }

  // 重试队列处理
  async processRetryQueue() {
    const now = Date.now();
    const itemsToRetry = [];

    for (const [key, item] of this.syncQueues) {
      if (item.nextRetryTime <= now && item.retryCount < 5) {
        itemsToRetry.push({ key, item });
      }
    }

    for (const { key, item } of itemsToRetry) {
      try {
        await this.manager.write(item.table, item.data, item.operation);
        this.syncQueues.delete(key);
      } catch (error) {
        item.retryCount++;
        item.nextRetryTime = Date.now() + Math.pow(2, item.retryCount) * 1000;
      }
    }
  }
}

冲突解决策略

在双写场景下,如何处理并发写入冲突是关键问题:

// 冲突解决器
class ConflictResolver {
  constructor(strategy = 'last-write-wins') {
    this.strategy = strategy;
    this.customRules = new Map();
  }

  // 注册自定义冲突规则
  registerRule(table, handler) {
    this.customRules.set(table, handler);
  }

  // 解决冲突
  async resolve(source1, source2, table) {
    // 检查是否有自定义规则
    if (this.customRules.has(table)) {
      return await this.customRules.get(table)(source1, source2);
    }

    switch (this.strategy) {
      case 'last-write-wins':
        return this.lastWriteWins(source1, source2);

      case 'source-priority':
        return this.sourcePriority(source1, source2);

      case 'merge':
        return await this.merge(source1, source2);

      case 'manual':
        return this.manual(source1, source2);

      default:
        return this.lastWriteWins(source1, source2);
    }
  }

  // 最后写入胜出
  lastWriteWins(source1, source2) {
    const time1 = new Date(source1.metadata.updatedAt).getTime();
    const time2 = new Date(source2.metadata.updatedAt).getTime();
    return time1 > time2 ? source1 : source2;
  }

  // 源优先级
  sourcePriority(source1, source2) {
    const priority = { 'primary': 1, 'secondary': 2, 'tertiary': 3 };
    const p1 = priority[source1.metadata.source] || 99;
    const p2 = priority[source2.metadata.source] || 99;
    return p1 <= p2 ? source1 : source2;
  }

  // 合并策略
  async merge(source1, source2) {
    const merged = { ...source1 };

    for (const key of Object.keys(source2)) {
      // 字段级别比较时间戳
      if (source2.metadata && source2.metadata.fields[key]) {
        const time1 = source1.metadata.fields[key]?.updatedAt || 0;
        const time2 = source2.metadata.fields[key]?.updatedAt || 0;

        if (time2 > time1) {
          merged[key] = source2[key];
        }
      } else if (!(key in source1)) {
        merged[key] = source2[key];
      }
    }

    return merged;
  }

  // 标记需要人工处理
  manual(source1, source2) {
    return {
      conflict: true,
      sources: [source1, source2],
      requireManualResolution: true
    };
  }
}

// 版本向量用于冲突检测
class VersionVector {
  constructor() {
    this.versions = new Map();
  }

  // 更新版本
  update(source, timestamp) {
    const current = this.versions.get(source) || 0;
    this.versions.set(source, Math.max(current, timestamp));
  }

  // 检测冲突
  detectConflict(other) {
    const thisSources = new Set(this.versions.keys());
    const otherSources = new Set(other.versions.keys());

    // 检查共同源
    const commonSources = [...thisSources].filter(s => otherSources.has(s));

    for (const source of commonSources) {
      // 如果两个版本互相不知道对方的存在,则可能冲突
      if (!this.isDescendant(other, source) && !other.isDescendant(this, source)) {
        return true;
      }
    }

    return false;
  }

  isDescendant(other, source) {
    return this.versions.get(source) > other.versions.get(source);
  }
}

最佳实践建议

总结

实时数据同步与双写架构是企业高可用系统的关键技术:

实际落地时需要根据业务场景和技术团队能力选择合适的方案,并持续优化迭代。

← 下一篇:企业信息化系统数据中台架构设计与实现