在讨论数据库选型时,SQLite和Cassandra经常被放在对立面上比较:一个是最流行的嵌入式单机数据库,一个是为海量数据设计的分布式宽表存储。但真实的工程实践中,这两者并不是二选一的关系。在边缘计算、物联网数据采集、离线优先应用等场景里,用SQLite承担本地高频读写,用Cassandra承担跨地域的数据汇总与分发,是一种经过验证且成本可控的混合架构。本文将围绕这一思路,展开一次完整的实战分析。

一、先厘清两者的本质差异:嵌入单机与分布式集群
SQLite是一个零配置、无独立服务进程的嵌入式数据库,整个数据库就是一个文件,应用程序通过函数调用直接读写,没有网络开销。它采用行式存储,事务遵循严格的ACID语义,单机写入性能在配合WAL模式时可以达到每秒数万次。它的短板同样明显:不支持多机并发写入,一旦进程所在机器宕机,数据可用性就完全取决于这份文件是否安全。
Cassandra则走了一条完全不同的路线。它是基于Amazon Dynamo论文和Google Bigtable数据模型构建的分布式数据库,采用无主架构,所有节点地位平等,数据通过一致性哈希分布在整个集群中,副本数可配置。写入时只需达到指定数量的副本确认即可返回,这种可调一致性让它在海量写入场景下表现突出。代价是查询模型的限制:CQL语法只高效支持基于主键的查询,跨分区扫描和复杂JOIN都是性能黑洞。
理解了这些差异,就能明白为什么两者可以互补。SQLite适合做“最后一公里的存储”,即离用户或设备最近的那一层数据落盘;Cassandra适合做“数据汇聚层”,负责把分散在各节点的数据统一管理起来。两者之间需要一个同步机制衔接,这正是实战项目的核心。
二、实战架构设计:本地落盘加异步同步的双层模型
假设我们要做一个工业设备数据采集系统:几百个边缘网关分布在不同厂房,每个网关每秒接收数十条传感器数据,需要本地持久化、断网可用,同时所有数据最终要汇总到中心机房供全局分析。这个需求用纯SQLite做不到跨节点汇总,用纯Cassandra则无法应对断网场景,混合架构就成了自然选择。
整体设计分三层。边缘层每个网关运行一个采集程序,数据先写入本地SQLite,利用WAL模式保证写入速度和崩溃安全。同步层维护一个待发送队列,网络可用时批量把数据推送到Cassandra,推送成功后更新本地状态标记。中心层是Cassandra集群,负责存储全量数据并提供查询服务。
本地SQLite的表结构可以这样设计:
-- 数据表:存储采集记录
CREATE TABLE sensor_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
metric TEXT NOT NULL,
value REAL NOT NULL,
captured_at INTEGER NOT NULL, -- 毫秒时间戳
synced INTEGER DEFAULT 0 -- 0 未同步,1 已同步
);
-- 索引:加速未同步数据的捞取
CREATE INDEX idx_unsynced ON sensor_data(synced, id);
写入端的代码示例(以Python为例):
import sqlite3, time
def init_db(path):
conn = sqlite3.connect(path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
return conn
def insert_reading(conn, device_id, metric, value):
conn.execute(
"INSERT INTO sensor_data(device_id, metric, value, captured_at) VALUES (?,?,?,?)",
(device_id, metric, value, int(time.time() * 1000))
)
conn.commit()
这里有两个细节值得展开。第一,PRAGMA journal_mode=WAL开启了预写日志模式,读写可以并发进行,采集线程写入时不会阻塞本地的查询线程。第二,synced标记位是整个同步机制的锚点,它让SQLite兼任了消息队列的角色,避免了额外引入一个队列组件的复杂度。
三、同步层实现:批量推送、幂等写入与冲突处理
同步层的职责是从本地捞出未同步数据,推送到Cassandra,然后标记完成。听起来简单,但分布式环境下的各种异常都要考虑:网络闪断、Cassandra写入超时、推送成功但确认丢失等。核心原则是保证幂等,即同一条数据重复写入Cassandra不会产生副作用。
Cassandra端的建表语句:
CREATE TABLE edge_readings (
device_id TEXT,
day TEXT, -- 分区键的一部分,按天分区避免分区过大
captured_at TIMESTAMP,
metric TEXT,
value DOUBLE,
edge_seq BIGINT, -- 边缘节点生成的全局唯一序号
PRIMARY KEY ((device_id, day), captured_at, edge_seq)
);
主键中包含edge_seq,它由SQLite的自增id加上网关编号拼接而成,天然全局唯一。这样即使同一条数据被推送两次,Cassandra的upsert语义会让第二次写入覆盖第一次,结果不变,幂等性就此达成。
同步程序的参考实现:
from cassandra.cluster import Cluster
from cassandra.query import BatchStatement
BATCH_SIZE = 500
def sync_loop(conn, gateway_id):
cluster = Cluster(['cassandra-host-1', 'cassandra-host-2'])
session = cluster.connect('iot')
insert_sql = session.prepare(
"INSERT INTO edge_readings(device_id, day, captured_at, metric, value, edge_seq) "
"VALUES (?,?,?,?,?,?)"
)
while True:
rows = conn.execute(
"SELECT id, device_id, metric, value, captured_at FROM sensor_data "
"WHERE synced=0 ORDER BY id LIMIT ?", (BATCH_SIZE,)
).fetchall()
if not rows:
time.sleep(5)
continue
batch = BatchStatement()
for r in rows:
day = time.strftime('%Y-%m-%d', time.localtime(r[4] / 1000))
batch.add(insert_sql, (r[1], day,
datetime.fromtimestamp(r[4] / 1000), r[2], r[3],
int(f"{gateway_id}{r[0]}")))
try:
session.execute(batch)
conn.execute("UPDATE sensor_data SET synced=1 WHERE id <= ?", (rows[-1][0],))
conn.commit()
except Exception as e:
print("sync failed, retry later:", e)
time.sleep(10)
几个设计要点需要说明。批量写入使用BatchStatement可以减少网络往返,但要控制批次大小,Cassandra官方建议单个批次不超过几十KB,超过反而会触发警告甚至拒绝。标记同步状态时用id <= 最大id的范围更新,比逐条更新高效得多。异常处理采取整体重试策略,依赖前面设计的幂等性兜底,代码因此可以写得非常简单,不需要精细追踪哪条成功哪条失败。
冲突处理方面,由于每条数据带时间戳且由单一网关产生,天然不存在多源写冲突。如果业务升级为多个节点可能修改同一份数据,就需要引入LWW(最后写入胜出)策略,利用Cassandra的writetime机制,或者在同步层比较版本号后再决定是否写入。
四、故障恢复与运维要点
边缘节点最常见的问题是磁盘写满和SQLite文件损坏。针对前者,可以定期清理已同步的历史数据,例如每天删除synced=1且超过七天的记录,并对数据库执行VACUUM回收空间。针对后者,SQLite本身有很强的容错能力,但突然断电仍可能损坏WAL文件,稳妥做法是采集程序启动时先执行PRAGMA integrity_check,校验失败则从最近备份恢复。
Cassandra侧的运维重点在于数据模型是否符合查询模式。按天分区是关键决策,如果某个设备单日写入量极大导致分区超过百万行,就需要进一步细分到小时分区。另外,gc_grace_seconds的设置要结合删除策略调整,边缘节点延迟同步可能长达数小时,删除墓碑的宽限期必须大于最大可容忍的同步延迟,否则会出现删除数据复活的问题。
监控层面,建议每个网关上报三个指标:本地未同步记录数、同步批次成功率、SQLite文件大小。未同步记录数持续增长说明网络或Cassandra集群出了问题;文件大小持续增长则说明清理任务没有生效。这三个指标足以覆盖绝大多数故障场景的定位需求。
五、总结:什么时候适合这套混合架构
这套SQLite加Cassandra的组合并非万能。它最适合的特征是:数据产生在边缘、写入频繁、网络不可靠、全局需要汇总查询。物联网采集、离线优先的移动应用、门店POS系统都是典型代表。反过来说,如果你的数据天然集中在一个机房,且需要强一致的复杂事务,直接用PostgreSQL这类传统关系库会更省事;如果数据量不大又要求多节点实时共享,SQLite的方案也帮不上忙。
从实际运行效果看,这套架构的优势在于边缘写入延迟稳定在毫秒级,完全不受中心网络波动影响,而Cassandra集群的横向扩展能力让汇总层的容量规划变得简单。踩过的坑主要集中两点:一是Cassandra批次大小需要压测确定,盲目加大批次会适得其反;二是幂等设计必须在一开始就纳入表结构,事后补改的代价非常高。理解了这些边界和细节,这套混合方案就能在合适的场景里稳定发挥价值。