Supabase云同步架构:Flutter应用的数据同步策略

本文基于BeeCount(蜜蜂记账)项目的实际开发经验,深入探讨如何使用Supabase构建安全、高效的云端数据同步系统。

项目背景

BeeCount(蜜蜂记账)是一款开源、简洁、无广告的个人记账应用。所有财务数据完全由用户掌控,支持本地存储和可选的云端同步,确保数据绝对安全。

引言

在现代移动应用开发中,多设备数据同步已成为用户的基本需求。用户希望在手机、平板、不同设备间无缝切换,同时保持数据的一致性和安全性。BeeCount选择Supabase作为云端后台服务,不仅因为其开源特性和强大功能,更重要的是它提供了完整的数据安全保障。

Supabase架构优势

开源与自主可控

  • 开源透明:完全开源的后台即服务(BaaS)解决方案
  • 数据主权:支持自建部署,数据完全可控
  • 标准技术:基于PostgreSQL,无厂商锁定风险

功能完整性

  • 实时数据库:基于PostgreSQL的实时数据同步
  • 用户认证:完整的身份验证和授权系统
  • 文件存储:对象存储服务,支持大文件上传
  • 边缘函数:服务端逻辑处理能力

同步架构设计

整体架构图

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│ Flutter App │ │ Supabase │ │ Other Device │
│ (Local SQLite) │◄──►│ (PostgreSQL) │◄──►│ (Local SQLite) │
│ │ │ (Auth + Storage) │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
└───── 加密备份文件 ─────┴───── 加密备份文件 ─────┘

核心设计原则

  1. 本地优先:所有操作优先在本地完成,确保响应速度
  2. 增量同步:只同步变更数据,降低网络开销
  3. 端到端加密:敏感数据在客户端加密后上传
  4. 冲突处理:合理的冲突解决策略
  5. 离线可用:网络异常时应用仍可正常使用

认证系统集成

Supabase认证配置

class SupabaseAuthService implements AuthService {
final s.SupabaseClient client; SupabaseAuthService({required this.client}); @override
Future<AuthResult> signInWithEmail({
required String email,
required String password,
}) async {
try {
final response = await client.auth.signInWithPassword(
email: email,
password: password,
); if (response.user != null) {
return AuthResult.success(user: AppUser.fromSupabase(response.user!));
} else {
return AuthResult.failure(error: 'Login failed');
}
} catch (e) {
return AuthResult.failure(error: e.toString());
}
} @override
Future<AuthResult> signUpWithEmail({
required String email,
required String password,
}) async {
try {
final response = await client.auth.signUp(
email: email,
password: password,
); return AuthResult.success(user: AppUser.fromSupabase(response.user!));
} catch (e) {
return AuthResult.failure(error: e.toString());
}
} @override
Stream<AuthState> get authStateChanges {
return client.auth.onAuthStateChange.map((data) {
if (data.session?.user != null) {
return AuthState.authenticated(
user: AppUser.fromSupabase(data.session!.user)
);
}
return AuthState.unauthenticated();
});
}
}

用户模型设计

class AppUser {
final String id;
final String email;
final DateTime? lastSignInAt; const AppUser({
required this.id,
required this.email,
this.lastSignInAt,
}); factory AppUser.fromSupabase(s.User user) {
return AppUser(
id: user.id,
email: user.email ?? '',
lastSignInAt: user.lastSignInAt,
);
} bool get isAnonymous => email.isEmpty;
}

数据同步策略

备份文件格式

BeeCount采用加密备份文件的方式进行数据同步:

class BackupData {
final String version;
final DateTime createdAt;
final String deviceId;
final Map<String, dynamic> ledgers;
final Map<String, dynamic> accounts;
final Map<String, dynamic> categories;
final Map<String, dynamic> transactions; BackupData({
required this.version,
required this.createdAt,
required this.deviceId,
required this.ledgers,
required this.accounts,
required this.categories,
required this.transactions,
}); Map<String, dynamic> toJson() => {
'version': version,
'createdAt': createdAt.toIso8601String(),
'deviceId': deviceId,
'ledgers': ledgers,
'accounts': accounts,
'categories': categories,
'transactions': transactions,
}; factory BackupData.fromJson(Map<String, dynamic> json) {
return BackupData(
version: json['version'],
createdAt: DateTime.parse(json['createdAt']),
deviceId: json['deviceId'],
ledgers: json['ledgers'],
accounts: json['accounts'],
categories: json['categories'],
transactions: json['transactions'],
);
}
}

同步服务实现

class SupabaseSyncService implements SyncService {
final s.SupabaseClient client;
final BeeDatabase db;
final BeeRepository repo;
final AuthService auth;
final String bucket; // 状态缓存和上传窗口管理
final Map<int, SyncStatus> _statusCache = {};
final Map<int, _RecentUpload> _recentUpload = {};
final Map<int, DateTime> _recentLocalChangeAt = {}; SupabaseSyncService({
required this.client,
required this.db,
required this.repo,
required this.auth,
this.bucket = 'beecount-backups',
}); @override
Future<SyncStatus> getSyncStatus(int ledgerId) async {
// 检查缓存
if (_statusCache.containsKey(ledgerId)) {
final cached = _statusCache[ledgerId]!;
if (DateTime.now().difference(cached.lastCheck).inMinutes < 5) {
return cached;
}
} try {
final user = await auth.getCurrentUser();
if (user == null) {
return SyncStatus.notLoggedIn();
} // 获取云端文件信息
final fileName = 'ledger_${ledgerId}_backup.json';
final cloudFile = await _getCloudFileInfo(fileName); // 计算本地数据指纹
final localFingerprint = await _calculateLocalFingerprint(ledgerId); if (cloudFile == null) {
final status = SyncStatus.localOnly(
localFingerprint: localFingerprint,
hasLocalChanges: true,
);
_statusCache[ledgerId] = status;
return status;
} // 比较指纹判断同步状态
final isUpToDate = localFingerprint == cloudFile.fingerprint;
final status = SyncStatus.synced(
localFingerprint: localFingerprint,
cloudFingerprint: cloudFile.fingerprint,
isUpToDate: isUpToDate,
lastSyncAt: cloudFile.lastModified,
); _statusCache[ledgerId] = status;
return status;
} catch (e) {
logger.error('Failed to get sync status', e);
return SyncStatus.error(error: e.toString());
}
} @override
Future<SyncResult> uploadBackup(int ledgerId) async {
try {
final user = await auth.getCurrentUser();
if (user == null) {
return SyncResult.failure(error: 'Not logged in');
} // 生成备份数据
final backupData = await _generateBackup(ledgerId);
final jsonString = json.encode(backupData.toJson()); // 加密备份数据
final encryptedData = await _encryptBackupData(jsonString); // 上传到Supabase Storage
final fileName = 'ledger_${ledgerId}_backup.json';
final uploadResult = await client.storage
.from(bucket)
.uploadBinary(fileName, encryptedData); if (uploadResult.isNotEmpty) {
// 记录上传成功
final fingerprint = await _calculateLocalFingerprint(ledgerId);
_recentUpload[ledgerId] = _RecentUpload(
fingerprint: fingerprint,
uploadedAt: DateTime.now(),
); // 更新缓存
_statusCache[ledgerId] = SyncStatus.synced(
localFingerprint: fingerprint,
cloudFingerprint: fingerprint,
isUpToDate: true,
lastSyncAt: DateTime.now(),
); return SyncResult.success(
syncedAt: DateTime.now(),
message: 'Backup uploaded successfully',
);
} return SyncResult.failure(error: 'Upload failed');
} catch (e) {
logger.error('Failed to upload backup', e);
return SyncResult.failure(error: e.toString());
}
} @override
Future<SyncResult> downloadRestore(int ledgerId) async {
try {
final user = await auth.getCurrentUser();
if (user == null) {
return SyncResult.failure(error: 'Not logged in');
} // 下载备份文件
final fileName = 'ledger_${ledgerId}_backup.json';
final downloadData = await client.storage
.from(bucket)
.download(fileName); if (downloadData.isEmpty) {
return SyncResult.failure(error: 'No backup found');
} // 解密备份数据
final decryptedData = await _decryptBackupData(downloadData);
final backupData = BackupData.fromJson(json.decode(decryptedData)); // 执行数据恢复
await _restoreFromBackup(backupData, ledgerId); // 更新状态
final fingerprint = await _calculateLocalFingerprint(ledgerId);
_statusCache[ledgerId] = SyncStatus.synced(
localFingerprint: fingerprint,
cloudFingerprint: fingerprint,
isUpToDate: true,
lastSyncAt: DateTime.now(),
); return SyncResult.success(
syncedAt: DateTime.now(),
message: 'Data restored successfully',
);
} catch (e) {
logger.error('Failed to download restore', e);
return SyncResult.failure(error: e.toString());
}
} // 数据加密/解密
Future<Uint8List> _encryptBackupData(String jsonData) async {
final key = await _getDerivedKey();
final cipher = AESCipher(key);
return cipher.encrypt(utf8.encode(jsonData));
} Future<String> _decryptBackupData(Uint8List encryptedData) async {
final key = await _getDerivedKey();
final cipher = AESCipher(key);
final decrypted = cipher.decrypt(encryptedData);
return utf8.decode(decrypted);
}
}

数据安全保障

端到端加密

class AESCipher {
final Uint8List key; AESCipher(this.key); Uint8List encrypt(List<int> plaintext) {
final cipher = AESEngine()
..init(true, KeyParameter(key)); // 生成随机IV
final iv = _generateRandomIV();
final cbcCipher = CBCBlockCipher(cipher)
..init(true, ParametersWithIV(KeyParameter(key), iv)); // PKCS7填充
final paddedPlaintext = _padPKCS7(Uint8List.fromList(plaintext));
final ciphertext = Uint8List(paddedPlaintext.length); for (int i = 0; i < paddedPlaintext.length; i += 16) {
cbcCipher.processBlock(paddedPlaintext, i, ciphertext, i);
} // IV + 密文
return Uint8List.fromList([...iv, ...ciphertext]);
} Uint8List decrypt(Uint8List encrypted) {
// 分离IV和密文
final iv = encrypted.sublist(0, 16);
final ciphertext = encrypted.sublist(16); final cipher = AESEngine()
..init(false, KeyParameter(key));
final cbcCipher = CBCBlockCipher(cipher)
..init(false, ParametersWithIV(KeyParameter(key), iv)); final decrypted = Uint8List(ciphertext.length);
for (int i = 0; i < ciphertext.length; i += 16) {
cbcCipher.processBlock(ciphertext, i, decrypted, i);
} // 移除PKCS7填充
return _removePKCS7Padding(decrypted);
} Uint8List _generateRandomIV() {
final random = Random.secure();
return Uint8List.fromList(
List.generate(16, (_) => random.nextInt(256))
);
}
}

密钥派生

Future<Uint8List> _getDerivedKey() async {
final user = await auth.getCurrentUser();
if (user == null) throw Exception('User not authenticated'); // 使用用户ID和设备特征生成密钥
final salt = utf8.encode('${user.id}_${await _getDeviceId()}');
final password = utf8.encode(user.id); // PBKDF2密钥派生
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))
..init(Pbkdf2Parameters(salt, 10000, 32)); return pbkdf2.process(password);
} Future<String> _getDeviceId() async {
final prefs = await SharedPreferences.getInstance();
String? deviceId = prefs.getString('device_id'); if (deviceId == null) {
deviceId = const Uuid().v4();
await prefs.setString('device_id', deviceId);
} return deviceId;
}

冲突处理策略

冲突检测

class ConflictDetector {
static ConflictResolution detectConflict({
required BackupData localData,
required BackupData cloudData,
required DateTime lastSyncAt,
}) {
final localChanges = <String, dynamic>{};
final cloudChanges = <String, dynamic>{}; // 检测交易记录冲突
_detectTransactionConflicts(
localData.transactions,
cloudData.transactions,
lastSyncAt,
localChanges,
cloudChanges,
); if (localChanges.isEmpty && cloudChanges.isEmpty) {
return ConflictResolution.noConflict();
} if (localChanges.isNotEmpty && cloudChanges.isEmpty) {
return ConflictResolution.localWins(changes: localChanges);
} if (localChanges.isEmpty && cloudChanges.isNotEmpty) {
return ConflictResolution.cloudWins(changes: cloudChanges);
} // 存在双向冲突,需要用户选择
return ConflictResolution.needsResolution(
localChanges: localChanges,
cloudChanges: cloudChanges,
);
} static void _detectTransactionConflicts(
Map<String, dynamic> localTxs,
Map<String, dynamic> cloudTxs,
DateTime lastSyncAt,
Map<String, dynamic> localChanges,
Map<String, dynamic> cloudChanges,
) {
// 检测本地新增/修改的交易
localTxs.forEach((id, localTx) {
final txUpdatedAt = DateTime.parse(localTx['updatedAt'] ?? localTx['createdAt']);
if (txUpdatedAt.isAfter(lastSyncAt)) {
localChanges[id] = localTx;
}
}); // 检测云端新增/修改的交易
cloudTxs.forEach((id, cloudTx) {
final txUpdatedAt = DateTime.parse(cloudTx['updatedAt'] ?? cloudTx['createdAt']);
if (txUpdatedAt.isAfter(lastSyncAt)) {
cloudChanges[id] = cloudTx;
}
});
}
}

冲突解决

class ConflictResolver {
static Future<BackupData> resolveConflict({
required BackupData localData,
required BackupData cloudData,
required ConflictResolution resolution,
}) async {
switch (resolution.type) {
case ConflictType.noConflict:
return localData; case ConflictType.localWins:
return localData; case ConflictType.cloudWins:
return cloudData; case ConflictType.needsResolution:
return await _mergeData(localData, cloudData, resolution);
}
} static Future<BackupData> _mergeData(
BackupData localData,
BackupData cloudData,
ConflictResolution resolution,
) async {
// 实现智能合并策略
final mergedTransactions = <String, dynamic>{}; // 优先保留较新的数据
mergedTransactions.addAll(cloudData.transactions); resolution.localChanges.forEach((id, localTx) {
final localUpdatedAt = DateTime.parse(localTx['updatedAt'] ?? localTx['createdAt']); if (resolution.cloudChanges.containsKey(id)) {
final cloudTx = resolution.cloudChanges[id];
final cloudUpdatedAt = DateTime.parse(cloudTx['updatedAt'] ?? cloudTx['createdAt']); // 保留时间戳较新的版本
if (localUpdatedAt.isAfter(cloudUpdatedAt)) {
mergedTransactions[id] = localTx;
} else {
mergedTransactions[id] = cloudTx;
}
} else {
mergedTransactions[id] = localTx;
}
}); return BackupData(
version: localData.version,
createdAt: DateTime.now(),
deviceId: localData.deviceId,
ledgers: localData.ledgers,
accounts: localData.accounts,
categories: localData.categories,
transactions: mergedTransactions,
);
}
}

性能优化

增量同步优化

class IncrementalSync {
static Future<BackupData> generateIncrementalBackup({
required int ledgerId,
required DateTime lastSyncAt,
required BeeRepository repo,
}) async {
// 只获取自上次同步后的变更数据
final changedTransactions = await repo.getTransactionsSince(
ledgerId: ledgerId,
since: lastSyncAt,
); final changedAccounts = await repo.getAccountsSince(
ledgerId: ledgerId,
since: lastSyncAt,
); // 构建增量备份数据
return BackupData(
version: '1.0',
createdAt: DateTime.now(),
deviceId: await _getDeviceId(),
ledgers: {}, // 账本信息变化较少,可按需包含
accounts: _mapAccountsToJson(changedAccounts),
categories: {}, // 分类变化较少,可按需包含
transactions: _mapTransactionsToJson(changedTransactions),
);
} static Map<String, dynamic> _mapTransactionsToJson(List<Transaction> transactions) {
return Map.fromEntries(
transactions.map((tx) => MapEntry(
tx.id.toString(),
{
'id': tx.id,
'ledgerId': tx.ledgerId,
'type': tx.type,
'amount': tx.amount,
'categoryId': tx.categoryId,
'accountId': tx.accountId,
'toAccountId': tx.toAccountId,
'happenedAt': tx.happenedAt.toIso8601String(),
'note': tx.note,
'updatedAt': DateTime.now().toIso8601String(),
}
))
);
}
}

网络优化

class NetworkOptimizer {
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 2); static Future<T> withRetry<T>(Future<T> Function() operation) async {
int attempts = 0; while (attempts < maxRetries) {
try {
return await operation();
} catch (e) {
attempts++; if (attempts >= maxRetries) {
rethrow;
} // 指数退避
await Future.delayed(retryDelay * (1 << attempts));
}
} throw Exception('Max retries exceeded');
} static Future<bool> isNetworkAvailable() async {
try {
final result = await InternetAddress.lookup('supabase.co');
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
} catch (_) {
return false;
}
}
}

用户体验设计

同步状态展示

class SyncStatusWidget extends ConsumerWidget {
final int ledgerId; const SyncStatusWidget({Key? key, required this.ledgerId}) : super(key: key); @override
Widget build(BuildContext context, WidgetRef ref) {
final syncStatus = ref.watch(syncStatusProvider(ledgerId)); return syncStatus.when(
data: (status) => _buildStatusIndicator(status),
loading: () => const SyncLoadingIndicator(),
error: (error, _) => SyncErrorIndicator(error: error.toString()),
);
} Widget _buildStatusIndicator(SyncStatus status) {
switch (status.type) {
case SyncStatusType.synced:
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_done, color: Colors.green, size: 16),
SizedBox(width: 4),
Text('已同步', style: TextStyle(fontSize: 12, color: Colors.green)),
],
); case SyncStatusType.localOnly:
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_off, color: Colors.orange, size: 16),
SizedBox(width: 4),
Text('仅本地', style: TextStyle(fontSize: 12, color: Colors.orange)),
],
); case SyncStatusType.needsUpload:
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_upload, color: Colors.blue, size: 16),
SizedBox(width: 4),
Text('待上传', style: TextStyle(fontSize: 12, color: Colors.blue)),
],
); case SyncStatusType.needsDownload:
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_download, color: Colors.purple, size: 16),
SizedBox(width: 4),
Text('有更新', style: TextStyle(fontSize: 12, color: Colors.purple)),
],
); default:
return SizedBox.shrink();
}
}
}

同步操作界面

class SyncActionsSheet extends ConsumerWidget {
final int ledgerId; const SyncActionsSheet({Key? key, required this.ledgerId}) : super(key: key); @override
Widget build(BuildContext context, WidgetRef ref) {
final syncStatus = ref.watch(syncStatusProvider(ledgerId)); return DraggableScrollableSheet(
initialChildSize: 0.4,
minChildSize: 0.2,
maxChildSize: 0.8,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// 拖拽指示器
Container(
width: 40,
height: 4,
margin: EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
), // 标题
Text(
'云端同步',
style: Theme.of(context).textTheme.headlineSmall,
), Expanded(
child: ListView(
controller: scrollController,
padding: EdgeInsets.all(16),
children: [
_buildSyncActions(context, ref, syncStatus),
],
),
),
],
),
);
},
);
} Widget _buildSyncActions(BuildContext context, WidgetRef ref, AsyncValue<SyncStatus> syncStatus) {
return syncStatus.when(
data: (status) {
switch (status.type) {
case SyncStatusType.localOnly:
return Column(
children: [
_buildActionCard(
title: '上传备份',
subtitle: '将本地数据上传到云端',
icon: Icons.cloud_upload,
color: Colors.blue,
onTap: () => _uploadBackup(ref),
),
],
); case SyncStatusType.needsDownload:
return Column(
children: [
_buildActionCard(
title: '下载恢复',
subtitle: '从云端恢复数据(会覆盖本地数据)',
icon: Icons.cloud_download,
color: Colors.purple,
onTap: () => _downloadRestore(context, ref),
),
SizedBox(height: 16),
_buildActionCard(
title: '强制上传',
subtitle: '用本地数据覆盖云端备份',
icon: Icons.upload,
color: Colors.orange,
onTap: () => _forceUpload(context, ref),
),
],
); default:
return _buildSyncInfo(status);
}
},
loading: () => Center(child: CircularProgressIndicator()),
error: (error, _) => Text('错误: $error'),
);
} Widget _buildActionCard({
required String title,
required String subtitle,
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: color.withOpacity(0.1),
child: Icon(icon, color: color),
),
title: Text(title),
subtitle: Text(subtitle),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: onTap,
),
);
}
}

最佳实践总结

1. 架构设计原则

  • 本地优先:确保应用离线可用
  • 渐进式同步:支持部分同步,不影响核心功能
  • 状态透明:让用户清楚了解同步状态

2. 安全性考虑

  • 端到端加密:敏感数据客户端加密
  • 密钥管理:使用安全的密钥派生和存储
  • 权限控制:确保用户只能访问自己的数据

3. 性能优化

  • 增量同步:只传输变更数据
  • 压缩上传:减少网络传输量
  • 后台同步:不影响用户操作

4. 用户体验

  • 状态可见:清晰的同步状态指示
  • 操作简单:一键同步,自动处理
  • 错误友好:明确的错误提示和恢复建议

实际应用效果

在BeeCount项目中,Supabase云同步系统带来了显著的价值:

  1. 用户满意度提升:多设备无缝切换,用户数据永不丢失
  2. 技术债务减少:基于成熟的BaaS服务,减少自建后台成本
  3. 安全性保障:端到端加密确保财务数据安全
  4. 开发效率:快速集成,专注业务逻辑开发

结语

Supabase作为开源的BaaS解决方案,为Flutter应用提供了完整的后端服务能力。通过合理的架构设计、安全的加密策略和良好的用户体验设计,我们可以构建出既安全又好用的云同步功能。

BeeCount的实践证明,选择合适的技术栈和设计模式,能够在保证数据安全的前提下,为用户提供便捷的多设备同步体验。这对于任何需要数据同步的应用都具有重要的参考价值。

关于BeeCount项目

项目特色

  • 现代架构: 基于Riverpod + Drift + Supabase的现代技术栈
  • 跨平台支持: iOS、Android双平台原生体验
  • 云端同步: 支持多设备数据实时同步
  • 个性化定制: Material Design 3主题系统
  • 数据分析: 完整的财务数据可视化
  • 国际化: 多语言本地化支持

技术栈一览

  • 框架: Flutter 3.6.1+ / Dart 3.6.1+
  • 状态管理: Flutter Riverpod 2.5.1
  • 数据库: Drift (SQLite) 2.20.2
  • 云服务: Supabase 2.5.6
  • 图表: FL Chart 0.68.0
  • CI/CD: GitHub Actions

开源信息

BeeCount是一个完全开源的项目,欢迎开发者参与贡献:

参考资源

官方文档

学习资源


本文是BeeCount技术文章系列的第3篇,后续将深入探讨主题系统、数据可视化等话题。如果你觉得这篇文章有帮助,欢迎关注项目并给个Star!

Supabase云同步架构:Flutter应用的数据同步策略的更多相关文章

  1. GoldenGate 12.3 MA架构介绍系列(2) - 数据同步测试

    安装配置可参考上一篇:http://www.cnblogs.com/margiex/p/8071957.html 安装完成之后,会自动启动ServiceManager服务,此时,可以通过浏览器访问. ...

  2. 增量数据同步中间件DataLink分享(已开源)

    项目介绍 名称: DataLink['deitə liŋk]译意: 数据链路,数据(自动)传输器语言: 纯java开发(JDK1.8+)定位: 满足各种异构数据源之间的实时增量同步,一个分布式.可扩展 ...

  3. 分布式数据库数据从属与client与server的数据同步

    老实说,眼下市面上很多产品,的确是不成熟的产品. 用过一些,给人蛋痛的感觉. 导言 分布还是集总 今天我们来探讨一个非常重要的问题. 每一个程序猿都有其思想,我的思想之中的一个,就是分布式. 分布式, ...

  4. MongoDB副本集配置系列十一:MongoDB 数据同步原理和自动故障转移的原理

    1:数据同步的原理: 当Primary节点完成数据操作后,Secondary会做出一系列的动作保证数据的同步: 1:检查自己local库的oplog.rs集合找出最近的时间戳. 2:检查Primary ...

  5. Oracle和Elasticsearch数据同步

    Python编写Oracle和Elasticsearch数据同步脚本 标签: elasticsearchoraclecx_Oraclepython数据同步    Python知识库 一.版本 Pyth ...

  6. Others-Goldengate 数据同步

    GoldenGate 是一家创建于1995年的美国公司,开发总部设在旧金山,在北美,欧洲和亚洲(包括新加坡.印度.澳大利亚)设有支持中心. 公司名称 GoldenGate 总部地点 旧金山 成立时间 ...

  7. rsync数据同步工具的配置

    rsync数据同步工具的配置 1. rsync介绍 1.1.什么是rsync rsync是一款开源的快速的,多功能的,可实现全量及增量的本地或远程数据同步备份的优秀工具.Rsync软件适用于 unix ...

  8. rsync安装与配置使用 数据同步方案(centos6.5)

    rsync + crond   ==定时数据同步 sersync(inotify)  + rsync  ==实时数据同步,利用rsync实现 ##应用场景 ..1 主备服务器之间同步数据定时    = ...

  9. 云方案,依托H3C彩虹云存储架构,结合UIA统一认证系统,实现了用户数据的集中存储和管理

    客户的声音 资料云项目在迷你云基础上二次开发,通过使用云存储技术及文件秒传技术,对文件进行统一存储与管理,以达到节约文件管理成本.存储成本目的:通过有效的文件版本控制机制,以达到风险管控的目的:通过多 ...

  10. 大数据时代数据库-云HBase架构&生态&实践

    业务的挑战 存储量量/并发计算增大 现如今大量的中小型公司并没有大规模的数据,如果一家公司的数据量超过100T,且能通过数据产生新的价值,基本可以说是大数据公司了 .起初,一个创业公司的基本思路就是首 ...

随机推荐

  1. MyBatis时区问题

    (事不过三!!!!第二次犯时区的错误了) 关于根据MyBatis官方文档 配置全局xml文件时遇到的问题: org.apache.ibatis.exceptions.PersistenceExcept ...

  2. 在 django-ninja 中实现类似腾讯阿里云的应用鉴权机制

    前言 本文章介绍如何使用基于 AppClient 模型的 Django-Ninja API 鉴权机制. 这也是上次说的中台项目衍生物 中台项目相关的文章,我大概还会再写一篇 这个系列的文章注定是没什么 ...

  3. js 获取函数的调用者

    办法 严格模式arguments.callee.caller不给使用了 歪路子截取new Error()的报错字符串stack const getCall = ()=>{ let callArr ...

  4. vue的【选项式api】 vs 【组合式api】

    前言 伴随着新到的vue3,我们编写组件的书写方式也发生了变化. 除了底层的更新,编写方式的改变 或许才是我们最能直观感受到的. 其实就是vue3多了一种名为组合式api(composables ap ...

  5. CF1989E Distance to Different 题解

    CF1989E Distance to Different 好题.题目要求 \(b\) 数组的数量,但 \(b\) 数组难以直接计算,而 \(b\) 数组和 \(a\) 数组的映射关系并不明显.考虑分 ...

  6. LB 面试

    稀奇古怪 进了终面... 群面 感觉 没有华为正规 1.为了 离婚买房 合适么?? 2.技术面因为面我的是一个  java 所以 就没怎么问我.

  7. win11专业版打开安全中心变成应用商店的问题

    有一位雨林木风官网的小伙伴,反馈这么一个问题,当他用win11专业版系统的时候,点击安全中心时,会有提示要求您启动应用商店,不知道是哪里出了问题!面对这个问题,很多人不知道如何解决.然后本期win11 ...

  8. js中一个移除对象中子数组中空值的函数

    js中一个移除对象中子集数组中空值(null,undefined)的函数 function removeNull(obj){ let delarr = []; for(let i in obj){ / ...

  9. Dots生命周期和Mono生命周期对照

  10. Linux 文件IO操作

    1.打开和关闭文件 1.1 open #include<unistd.h> #include<fcntl.h> int open(const char *pathname, i ...