本文所讨论的方案,来源于笔者在 UrnaDB 数据库设计过程中的问题抽象与实践总结。
事务抽象
事务 Transaction 是数据库系统中的基础机制,Transaction 用于保证数据操作时的正确性与可靠性而提出的一组核心约束。事务必须具备的 ACID 四个核心特性,ACID 的缩写缩写源自于:原子性 Atomicity、一致性 Consistency、隔离性 Isolation 和持久性 Durability。ACID 约束确保了一组数据库操作,即使在运算故障时也能保持数据的一致性和可靠性,实现数据从一个合法状态向另一个合法状态的转换,使其执行运算过程中没有异常状态的产生。

目前工业级主流数据库产品 MySQL、PostgreSQL、Redis、MongoDB、DynamoDB 都有对 Transaction 特性的支持,但具体实现细节有着天壤之别。同样按照数据库类型分类:关系型数据库如 PostgreSQL 的事务实现通常基于 SQL 标准并完整支持 ACID 特性;而 NoSQL 数据库如 Redis、DynamoDB 的事务机制则没有统一标准,通常根据系统设计在原子性、隔离性等方面提供不同级别的支持,部分系统甚至仅支持单操作原子性。
关系型数据库的 SQL 语言表达能力强大,SQL 可以通过复杂的 JOIN、子查询、聚合函数、窗口函数等表达复杂的业务逻辑,一条 SQL 语句就能完成多表关联、分组统计、排序分页等操作。使用者就需要花费大量的时间来学习和掌握使用 SQL 语法包括复杂的子查询、窗口函数、索引原理、执行计划分析、事务隔离级别、锁机制、MVCC 原理、查询优化技巧、性能调优、高可用架构等。
NoSQL 相较于关系型 SQL 数据库在实现层面通常更加简化,通过放弃通用 SQL 标准,转而采用自定义 API 来定义数据访问与事务语义。NoSQL 系统无需实现 SQL 解析器、查询优化器以及执行引擎等复杂核心组件,从而显著降低了系统实现的复杂度,这使得 NoSQL 数据库对于小规模团队或独立开发者而言更加可行。
🛡️ 安全性
本节将以数据库 “安全性” 为切入点,对比 UrnaDB 与其他竞品数据库在相同安全问题下的设计思路与解决方案。传统 SQL 关系型数据库在生产环境中,安全层面的风险是不可忽略的要素,其中 SQL 注入作为一种典型的应用层安全漏洞,长期以来是开发者需要重点防范的问题之一,由于缺乏内建防护机制,使用者需要在应用层额外处理,例如在 Java 中通过 Prepared Statement 对 SQL 语句进行预编译。同样 NoSQL 数据库也存在安全层面的风险,例如 Redis 通过内置 Lua VM 虚拟机来实现事务逻辑或扩展能力如脚本执行、存储过程等,虽然增强了灵活性,但也带来了额外的复杂性和潜在的安全风险。
下文将通过具体恶意代码示例,分析这些潜在的安全风险,如下:
-- 死循环攻击
while true do
redis.call('GET', 'key')
end
-- 内存炸弹
local t = {}
for i=1,10000000 do
t[i] = string.rep("x", 1000000)
end
-- DoS 攻击
for i=1,1000000 do
redis.call('KEYS', '*')
end上述任意代码,如果被恶意用户提交到 Redis 的 Lua VM 中执行,一定会导致系统行为不可预测,从而影响整体稳定性与调试难度,例如脚本中的异常逻辑如死循环或大量内存分配会耗尽数据库硬件资源,使 Lua VM 长时间被占用,进而阻塞其他脚本或相关操作的正常执行。
例如在 Redis 配置文件中的参数,可以设置 Lua VM 执行自定义脚本的超时时间:
lua-time-limit 5000 # 默认 5 秒另外一个问题是 Redis Lua 原子事务无回滚机制,在一组事务计算过程中出现意外中断,无法正常回滚到这组数据原始状态,一个实际案例如下:
-- 场景:转账操作
EVAL "
redis.call('DECRBY', 'account:A', 100) -- ✅ 成功:A 扣款
-- 这里发生错误(比如 B 账户不存在)
redis.call('INCRBY', 'account:B', 100) -- ❌ 失败
return 'done'
" 2 account:A account:B事务计算过程中异常中断之后,数据出现不一致性:
A 账户:-100. ✅ 已执行 => B 账户:失败. ❌ 未执行下面是一组 UrnaDB 与 Redis 事务完备性的数据对比表:
| 操作类型 | 是否幂等 | Redis Lua | UrnaDB |
|---|---|---|---|
| SET | ✅ 是 | ✅ 安全 | ✅ 安全 |
| DEL | ✅ 是 | ✅ 安全 | ✅ 安全 |
| Lua | ❌ 否 | ❌ 危险 | ✅ 安全 |
| INCR | ❌ 否 | ❌ 危险 | ✅ 安全 |
| LPUSH | ❌ 否 | ❌ 危险 | ✅ 安全 |
通过上述数据对比可以看出,UrnaDB 在事务设计上优于 Redis 的设计。
⏱️ 高性能
首先需要明确这里的 “高性能” 并非泛指 UrnaDB 的整体性能,而是聚焦于其在高并发场景下处理海量事务时的吞吐能力。数据库事务的处理性能不仅受磁盘 I/O、CPU 和 RAM 等硬件因素影响,还与数据库系统本身内部的设计密切相关,本节将忽略外部环境因素,重点从 UrnaDB 的数据内结构设计以及事务管理器、和锁机制出发,探讨其对 UrnaDB 事务吞吐能力的影响。
数据库通常通过内置的锁管理器来协调并发事务对共享数据的访问,以避免数据竞争并保证一致性,UrnaDB 数据库系统中多个事务在并发数据竞争示意图:

UrnaDB 中的批量原子事务操作是针对的 Table 命名空间中的数据记录所设计。在之前的 API 交互 文档中有提到 Table 命名空间在初始化完成之后会自动分配一个全局锁。在默认单个原子事务请求下,UrnaDB 内部的事务处理器会使用本次事务请求中所涉及到的 Table 对应锁进行并发数据竞争的保护,单个事务处理的示意图如下:

但在涉及到多个 Table 批量事务处理时候上述的设计就不适用了,跨多 Table 事务在执行过程中,需要同时获取并持有多个 Table 命名空间对应的锁,从而扩大锁作用域,锁范围的扩展会提高锁冲突概率,降低并发执行效率,并最终影响系统整体吞吐性能,多事务锁冲突示意图如下:

UrnaDB 的批量事务操作也参考了传统 SQL 数据库的 MVCC 的设计,在默认批量事务请求处理下也是使用的多个数据版本快照的方式供事务执行运算操作。多个并行处理的事务之间操作的数据都是 Snapshot 副本,每个事务的运算都是基于 Snapshot 快照副本,从而实现了无事务锁干预来提升数据库整体事务吞吐量,多个事务 MVCC 处理示意图如下:

需要注意的是在 MVCC 模式下,事务处理并不能保证每个事务都能够成功提交,当多个事务对同一数据记录产生写冲突时,系统会在提交阶段进行冲突检测并回滚冲突事务。在需要强一致性的批量事务场景下,UrnaDB 提供类似传统关系型数据库 Serialization 模式中两阶段锁 2PL 的事务处理模式,事务串行化示意图如下:

⚛️ 原子事务
前文从安全性与性能角度对不同类型数据库的设计优缺点进行了分析,在此基础上本节将进一步介绍 UrnaDB 的 Transaction 事务功能及其使用方法。对于用户而言,只需掌握相关 API 的使用方式,无需关注底层事务机制的实现细节,与其他事务相关 API 一致,UrnaDB 的 Transaction 事务操作同样基于 PQL 协议 实现。
例如下面展示的是一组 Mutations 结构请求的 JSON Body 抽象表示:
{
"mutations": [
{
"name": "users",
"operation": "INSERT",
"values": {
"id": 1,
"name": "Leon Ding",
"age": 25
}
},
{
"name": "users",
"operation": "UPDATE",
"where": {
"id": 1
},
"values": {
"age": 26
}
}
],
"serialization": false
}使用 UrnaDB 批量原子事务时,客户端只需向 https://.../txns 端点发送 HTTP 协议的 POST 请求,服务器端收到事务请求之后即可执行批量事务操作,需要注意的是事务中涉及的 Table 命名空间必须已提前创建;下面是使用 HTTP 客户端软件发送批量原子事务请求的示例:
curl -X POST "http://192.168.101.252:2668/txns" \
-H "Auth-Token: yv2PH82JXfm2UpScAQK37iJVI" \
-H "Content-Type: application/json" \
-d '{
"mutations": [
{
"name": "users",
"operation": "INSERT",
"values": {
"id": 1,
"name": "Leon Ding",
"age": 25
}
},
{
"name": "users",
"operation": "UPDATE",
"where": {
"id": 1
},
"values": {
"age": 26
}
}
],
"serialization": false
}'INSERT 和 UPDATE 组合事务语句,需要注意的是 serialization 字段的值,如果不传入 serialization 字段,UrnaDB 事务处理器默认会以值为 false 进行处理。此处的 serialization 对应前文提到的关系型数据库中的事务串行化模式,但在使用方式上更加灵活,关系型数据库通常需要在配置中统一设置事务隔离级别,而 UrnaDB 则通过在每次事务请求中指定 serialization 字段,动态决定是否启用串行化模式。
批量原子事务操作 JSON Body 请求中的 operation 字段用于标识当前 Mutation 的操作类型,不同类型对应不同的数据结构变更需求,客户端必须根据操作类型提供相应字段,否则事务请求将被拒绝;不同 Operation 类型说明表格如下:
| Operation | 描述 | 必填字段 | 说明 |
|---|---|---|---|
| INSERT | 插入数据 | name, values | 向指定 Table 写入一条或多条记录 |
| UPDATE | 更新数据 | name, where, values | 根据条件更新符合条件的数据 |
| REMOVE | 删除数据 | name, where | 根据条件删除数据 |
目前批量原子事务仅支持上述表格中列出的 Operation 类型,后续将逐步扩展更多复杂条件数据筛选能力,例如范围查询以及基于 <= 、>= 、!= 的条件过滤。
🆚 产品差异性
| 功能维度 | 传统 SQL 数据库 | UrnaDB |
|---|---|---|
| SQL 注入 | ⚠️ 高风险 | ✅ 无风险 |
| 性能可预测 | ❌ 不可预测 | ✅ 可预测 |
| 查询优化 | ⚠️ 复杂 | ✅ 无需优化,一级索引和可选择二级索引 |
| 事务隔离 | ⚠️ 4 种级别,复杂 | ✅ MVCC 简单,基于版本的冲突检测 |
| 解析开销 | ⚠️ 10-70ms | ✅ ≈ 0.3ms,简单的 JSON 解析器 |
| 锁竞争 | ⚠️ 严重 | ✅ MVCC 无锁读,可 2PL 锁升级串行化 |
| Schema 变更 | ⚠️ 锁表 | ✅ 无 Schema,动态修改 Schema 结构 |
| 维护成本 | ⚠️ 需要 DBA 团队 | ✅ 低维护量,一个人就能胜任 |
| 扩展性 | ⚠️ 困难 | ✅ 简单,按需通过配置启用插件功能 |
| 学习曲线 | ⚠️ 陡峭 | ✅ 平缓,导入对应语言 SDK 即可使用 |
| 功能维度 | UrnaDB | Redis |
|---|---|---|
| ACID 支持 | ✅ 完整支持 | ❌ 不支持,Lua 事务有被攻击风险 |
| 原子性 | ✅ 真正的原子性 | ❌ 无回滚机制 |
| 隔离性 | ✅ MVCC + 可选 2PL 模式 | ❌ 单线程串行 |
| 事务持久性 | ✅ WAL + .txn 文件 | ⚠️ 依赖 AOF + RDB |
| 事务回滚机制 | ✅ 自动回滚 | ❌ 无回滚 |
| 事务并发控制 | ✅ MVCC 乐观锁 | ❌ 单线程无并发 |
| 事务版本检测 | ✅ 自动版本检测和回滚 | ⚠️ 手动 WATCH 丢弃执行 |
| 事务快照隔离 | ✅ 自动快照隔离 | ❌ 需手动读取 |
| 跨多 Key 事务 | ✅ 支持 | ⚠️ Lua 脚本异常会导致数据不一致 |
| 事务实现 | ✅ 真正的事务 | ⚠️ 串行执行 |
| 数据压缩 | ✅ 支持 | ⚠️ 需配置 |
| 数据加密 | ✅ 静态加密 | ❌ 需第三方 |
| 垃圾回收 | ✅ 自动磁盘 GC | ⚠️ 内存淘汰策略 |
| 分布式锁 | ✅ 原生支持 | ⚠️ 需 Redlock 算法 |
| 强一致性 | ✅ ACID 保证 | ❌ 不适合 |
| 订单系统 | ✅ 事务支持 | ❌ 不适合 |
| 持久化方式 | ✅ 基于 LogStructuredFS 实时 | ⚠️ RDB + AOF |
| 默认持久化 | ✅ 始终持久化 | ❌ 默认仅内存 |
| 数据安全性 | ✅ 高,每次写入持久化 | ⚠️ 中,可能丢失数据 |