在信创项目越来越多的今天,不少团队面临一个现实问题:客户端需要离线运行,数据先落在本地,等网络恢复后再同步到服务端,而服务端指定的数据库是国产的神舟通用ShenTong。这种场景下,SQLite作为嵌入式数据库几乎是本地存储的首选,但它与ShenTong之间的数据同步、类型映射、批量写入等环节都有不少细节需要处理。本文结合一个完整的实战项目,把架构设计、连接配置、同步逻辑和性能优化逐一讲清楚。

一、为什么选择SQLite加ShenTong的组合
先说清楚这个组合的定位。SQLite是一个零配置的嵌入式数据库,整个数据库就是一个文件,不需要独立的服务进程,非常适合作为客户端或边缘节点的本地存储层。它的事务模型基于文件锁,写入性能在小数据量场景下非常出色,而且几乎所有的编程语言都有成熟的驱动支持。
神舟通用ShenTong则是面向服务端的国产关系型数据库,兼容主流SQL标准,支持完整的并发事务、备份恢复和权限体系,是信创名录中常见的替代方案。它的角色是中心数据仓库,负责多客户端数据的汇聚、统一查询和业务报表。两者一个管本地,一个管中心,职责互补。
这个架构的典型应用场景包括:野外巡检App在无网环境下采集数据,回到有网环境批量上传;生产车间的工控机本地记录设备参数,定期同步到数据中心;政务窗口系统在断网时提供应急录入能力,恢复后自动归档。这些场景的共同点是写多读少、网络不可靠、对数据一致性要求最终一致即可。
二、架构设计与连接配置
整体架构分为三层:本地SQLite负责数据采集与缓存;同步引擎负责差量提取、冲突处理和批量写入;服务端ShenTong负责数据落地与业务查询。同步引擎可以用Java编写,通过JDBC分别连接两端。下面是核心的连接代码。
import java.sql.*;
public class DbConnector {
// 连接本地SQLite数据库
public static Connection getSqliteConn() throws Exception {
Class.forName("org.sqlite.JDBC");
return DriverManager.getConnection("jdbc:sqlite:D:/data/local_cache.db");
}
// 连接神舟通用ShenTong数据库
public static Connection getShenTongConn() throws Exception {
Class.forName("com.oscar.Driver");
String url = "jdbc:oscar://192.168.1.100:2003/BUSINESS";
return DriverManager.getConnection(url, "appuser", "app@2024");
}
}
ShenTong的JDBC驱动类名是com.oscar.Driver,URL前缀为jdbc:oscar,默认端口2003,这些信息在官方驱动包的文档中可以确认。需要注意的是,驱动jar包需要从神舟通用的安装目录或官方渠道获取,并且版本要与数据库服务端匹配,否则可能出现握手失败的问题。SQLite端则推荐使用sqlite-jdbc这个开源驱动,直接通过Maven引入即可。
在本地表设计上,建议为每张业务表增加几个同步控制字段:local_id作为本地自增主键,sync_status标记同步状态(0未同步、1已同步、2冲突),update_time记录本地更新时间戳。这些字段是差量同步的基础,能避免每次全量比对带来的性能开销。
三、差量同步与冲突处理的实现
同步的第一步是从SQLite中提取未同步的记录。做法很简单,按sync_status和update_time过滤,配合分页批量读取,避免一次性加载过多数据导致内存压力。提取后通过批量插入写入ShenTong,写入成功再把本地状态改为已同步。完整的同步逻辑如下。
public class SyncEngine {
public void syncInspectionData() throws Exception {
Connection local = DbConnector.getSqliteConn();
Connection remote = DbConnector.getShenTongConn();
remote.setAutoCommit(false);
try {
// 1. 从SQLite提取未同步数据
PreparedStatement select = local.prepareStatement(
"SELECT local_id, device_no, value, update_time FROM inspection " +
"WHERE sync_status = 0 ORDER BY update_time LIMIT 500");
ResultSet rs = select.executeQuery();
// 2. 批量写入ShenTong
PreparedStatement insert = remote.prepareStatement(
"INSERT INTO inspection(device_no, value, update_time) VALUES (?, ?, ?)");
int count = 0;
while (rs.next()) {
insert.setString(1, rs.getString("device_no"));
insert.setDouble(2, rs.getDouble("value"));
insert.setTimestamp(3, rs.getTimestamp("update_time"));
insert.addBatch();
count++;
if (count % 100 == 0) {
insert.executeBatch();
}
}
insert.executeBatch();
remote.commit();
// 3. 回写本地同步状态
PreparedStatement update = local.prepareStatement(
"UPDATE inspection SET sync_status = 1 WHERE local_id = ?");
// 此处遍历已同步的local_id并执行更新,略
System.out.println("本次同步记录数: " + count);
} catch (Exception e) {
remote.rollback();
throw e;
} finally {
local.close();
remote.close();
}
}
}
这段代码有几个值得注意的点。第一,ShenTong端的写入放在一个事务里,配合executeBatch批量执行,比逐条insert快一个数量级以上。第二,回写本地状态这一步最好也在SQLite端用事务包裹,防止出现远端写入成功但本地状态未更新的情况。第三,如果对可靠性要求更高,可以在ShenTong端表上加一个sync_token唯一约束,用本地记录的时间戳加设备号生成,写入重复时捕获异常并跳过,实现幂等同步。
冲突处理是同步中最容易踩坑的部分。如果同一条业务数据可能在多个客户端被修改,简单的主键直接插入会失败。常见的策略有两种:一是以时间戳为准的last write wins,冲突时用新记录覆盖旧记录,实现简单但可能丢数据;二是在ShenTong端增加中间表,冲突数据先进中间表,由人工或业务规则裁决后再合并。对于巡检采集这类一次写入型的数据,直接用方案一即可;对于可编辑的档案类数据,建议用方案二。
四、性能优化与常见问题
实际压测中,影响同步速度的因素主要有三个:批量大小、事务粒度和网络往返次数。批量大小建议设在100到500之间,太小体现不出批量优势,太大则会占用过多内存并让单次事务变长。事务粒度方面,一次同步开一个事务即可,不要每批提交一次,否则ShenTong端的日志写入会成为瓶颈。实测在千兆内网环境下,单表十万条记录的同步时间可以从逐条写入的约90秒压缩到4秒左右,差距非常明显。
SQLite端的写入优化同样重要。采集程序频繁写入时,务必把多条插入放进同一个事务,因为SQLite默认每条insert都会触发一次磁盘刷盘,不裹事务的写入速度可能只有每秒几十条。另外可以打开WAL模式,让读写不互相阻塞,采集与同步可以并行进行。
// 开启SQLite的WAL模式,提升并发读写能力
Statement st = local.createStatement();
st.execute("PRAGMA journal_mode=WAL;");
st.execute("PRAGMA synchronous=NORMAL;");
st.close();
最后说几个常见问题。类型映射方面,SQLite是动态类型,ShenTong是强类型,DATE字段在SQLite中实际存的是TEXT,同步时要用getTimestamp或先格式化再解析,直接按整数取会得到错误结果。字符集方面,ShenTong默认编码要与客户端一致,建议统一使用UTF-8,避免中文乱码。驱动冲突方面,如果项目同时引入多个数据库驱动,注意ShenTong驱动与某些老版本驱动的类加载冲突,必要时用独立类加载器隔离。把这些细节处理好,SQLite与ShenTong的双层数据架构可以稳定支撑信创环境下的离线加在线混合业务。