多用户数据库源码包v7.90:并发控制、事务日志与部署避坑指南
发布时间:2026/10/9 17:20:09 锦皓数字建站

简介Absolute Database v7.90 多用户版完整源码包面向 Delphi 开发者解决嵌入式多用户数据库应用的开发与集成问题无需额外数据库服务端支持本地部署与离线环境适合桌面软件、工业管理、小型业务系统等场景。压缩包内共 567 个文件约 6.94MB核心文件为 pas/dpk/dpr 等 Delphi 单元与组件包工程sql/sqm 为 SQL 脚本与查询文件dfm 为可视化窗体布局h/c 等提供 C/C 接口chm/hlp 为帮助文档另有多个版本的编译批处理便于在不同工具链中构建。完整源码可深入研读多用户并发控制、事务日志、索引与查询优化等数据库内核实现随附的 XE2、BCB5、VS2010 等编译脚本及示例工程可帮助快速集成与二次定制目录组织对自学和排错都较友好。已有 262 人学习下载适合具备 Delphi 基础、希望掌握组件底层原理或需要离线多用户数据库支持的中高级开发者。1. 多用户数据库源码包 v7.90 到底解决什么问题一个做进销存的开发者遇到的最典型场景单机版跑得好好的桌面数据库改成几个人同时录入后开始频繁出现“文件被占用”“记录无法更新”甚至整个库文件直接损坏。换大型数据库又觉得重关键是业务代码里嵌的 SQL 几乎都要重写。这种卡在半中腰的处境正是 v7.90 这类多用户数据库源代码方案要接的活。它试图把传统嵌入式数据库往前推一步底层仍是文件存储但通过完善的锁管理、事务日志和可配置的并发策略让多个终端同时读写同一份数据。适合的场景是中小型桌面应用、局域网点位的进销存和工单系统以及不想引入独立数据库服务器、又必须支持多用户并发的团队。对于要深度定制数据库行为的人来说能拿到源码意味着可以改锁等待策略、改日志回收逻辑甚至裁剪模块这比面对一个黑匣子踏实得多。2. 从单机到多用户v7.90 的并发架构与工作原理2.1 文件型多用户的两个流派共享模式与服务进程模式多用户数据库在文件型方案上有两条常见的路。第一条是文件共享模式所有客户端直接打开同一个数据库文件靠操作系统文件锁和数据库自带的锁表来协调访问。优点是部署简单不需要额外安装服务程序缺点是锁粒度粗、网络异常时锁容易残留。v7.90 在此模式下强调“短事务”因为每条 SQL 都伴随一次锁申请与释放事务越长锁冲突概率越高。我一般建议这种模式下单事务控制在 200 毫秒内完成所有查询、更新都走索引绝不允许全表扫描混入写事务。第二条是服务进程模式由一个轻量服务进程集中管理数据库文件客户端通过网络协议发请求。这个方案锁管理集中在服务端可以做到行级锁、排队写请求还能在服务进程里做读写缓存性能比文件共享稳定很多。v7.90 的老版本在这个模式上曾经有过内存泄漏问题v7.90 主要修复了这一块并加了连接数上限的硬保护。实际部署时如果点位超过 5 个我会优先选服务进程模式宁可多写一个服务的启停脚本也不要让所有客户端直接怼着文件锁干活。2.2 事务日志与锁管理器并发读写的核心机制v7.90 的写路径是典型的“日志先行”客户端发起的写操作先写入事务日志文件再更新主数据文件。这样做的好处是崩溃恢复时不至于只恢复一半记录重启后通过日志回放把未完成的事务回滚掉。对应到代码层面每一条 UPDATE 或 INSERT 都会涉及三个动作写日志、修改内存页、标记脏页。如果进程在第二步和第三步之间崩溃日志就是唯一的后悔药。锁管理器则维护了一张“锁登记表”用锁粒度和锁模式来控制并发。v7.90 支持三种常见锁共享锁用于读独占锁用于写更新锁用于先读后写的场景。更新锁存在的意义是避免两个客户端同时读同一行、然后都尝试升级成独占锁造成死锁。这套机制单独看并不新鲜但它把锁超时时间暴露成了配置项这就给了调优空间。短超时适合点单类业务用户等不起长超时适合批量导入场景宁可等也不愿失败重来。2.3 多用户冲突检测提交时的版本比对很多文件型数据库的多用户支持是假的——后提交的覆盖先提交的数据悄悄丢失。v7.90 的提交协议里带了一个版本号字段客户端在开启事务时记录数据版本提交时服务端检查当前版本是否变化。如果发现版本不一致直接拒绝提交并返回错误码把这行数据留给客户端决定是重读还是强制覆盖。这个机制说起来轻巧做起来最影响体验。因为它要求应用层必须处理版本冲突错误。实际项目里很多团队在这个环节偷懒把冲突错误当成普通失败弹个框用户根本不知道数据已经被别人改过。正确的处理方式是捕获冲突错误后立即刷新当前行数据展示给用户“你编辑期间他人已做修改”让操作者决定是覆盖还是取消。v7.90 返回的错误码区分了“记录不存在”“版本冲突”“锁等待超时”这在排查并发问题时非常关键能直接从日志看出是锁的问题还是冲突策略的问题。3. 编译与接入把 v7.90 源码跑起来的最小路径3.1 源码目录结构与服务端启动拿到 v7.90 源码包后先别急着编译业务代码第一步是把内置服务端跑起来。常见做法是根目录下分几个子目录server 放服务进程源码client 放客户端接口common 放共享协议定义tools 放管理工具。编译前确认依赖的三方库已经安装然后先编 common 再编 server。cd Absolute_Database_Multi_User_Source_v7.90/common make clean make cd ../server make clean make ./db_server --config ./server.conf --daemon编译参数方面我一般会在 common 的 Makefile 里把协议缓冲区大小从默认 4KB 调到 16KB因为业务里经常有长备注字段缓冲区太小会导致大字段写入被拆包性能反而下降。db_server 启动参数里最需要注意的是--daemon它会脱离当前终端后台运行所有日志落到 server.conf 指定的路径而不是打印到屏幕上。首次调试时把--daemon去掉前台跑能看到更完整的启动日志排查配置问题快得多。3.2 必调的三个连接参数用户数、锁超时与缓存页server.conf 里决定多用户体感的参数有三个。第一个是最大连接数max_connections默认值通常是 50但连接数和并发事务数是两回事。连接只是 TCP 通道真正并发执行事务的量由max_concurrent_transactions控制设太大会导致锁等待明显变长设太小又浪费 CPU。我一般建议按实际业务峰值的 1.5 倍来设并发事务数比如最多 20 个人同时录单就设为 30。第二个是锁超时lock_timeout_ms默认 3000。局域网环境建议降到 1000因为网络延迟低1 秒内拿不到锁说明对方事务一定很长再等不如快速失败让用户重试。远程访问场景反而要调高到 5000 以上否则网络抖动一次就报锁超时。第三个是缓存页cache_pages默认 1024 页。这个值直接决定读多写少的业务能不能跑得动。每页默认 4KB1024 页就是 4MB 内存缓存。如果表数据总量超过 200MB我建议至少设成 32768也就是 128MB 缓存这样热表基本都在内存里查询响应可以快一个量级。注意这个值不是越大越好设太大反而会让操作系统频繁换页Linux 上用--cache_pages配完以后观察内存占用确保不超过物理内存的 30%。3.3 客户端连接示例与错误码约定客户端接口在不同语言里风格类似建立连接、开启事务、执行 SQL、提交或回滚。这里给一个 Python 客户端的常见写法模拟典型的“先查再改”流程。import dbclient_v790 as db conn db.connect(127.0.0.1, 54321, useroperator, passwordpass) conn.begin() try: row conn.execute(SELECT qty, version FROM stock WHERE id ?, (1001,)).fetchone() if row is None: raise RuntimeError(stock record missing) new_qty row[qty] - 5 if new_qty 0: raise RuntimeError(insufficient stock) conn.execute(UPDATE stock SET qty ?, version version 1 WHERE id ? AND version ?, (new_qty, 1001, row[version])) conn.commit() except db.VersionConflictError as e: conn.rollback() print(fconflict on record {e.record_id}, refresh and retry) except db.LockTimeoutError as e: conn.rollback() print(flock wait exceeded {e.timeout_ms}ms)逻辑上先查当前数量和版本号再按版本号做条件更新。WHERE id ? AND version ?是关键如果提交时版本号不匹配服务端会拒绝更新并抛出版本冲突异常。参数部分db.connect里的 54321 是 v7.90 默认服务端口user 和 password 的校验在服务端处理密码传输默认是明文跨公网必须配合加密通道这一点后面避坑章节还会提。另外这个示例故意用了参数化 SQL 而不是拼接字符串多用户写入场景下参数化能避免数据类型隐式转换带来的锁范围扩大这一点也值得注意。4. 多用户场景下的表设计与数据安全保障4.1 主键、索引与写事务的锁范围多用户数据库的性能瓶颈往往不在数据库引擎而在表设计。v7.90 的锁默认是行级的但有一个前提更新语句必须能通过索引精确定位到行。如果 UPDATE 的 WHERE 条件没走索引锁管理器会退化成锁定整个表两个客户端改不同记录也会互相阻塞表现就是越用越卡、越卡越锁。我常用的设计原则有三条。第一每张业务表必须有一个代理主键是自增整数或 UUID不要用业务字段组合当主键否则后续改业务规则时要动主键迁移成本极高。第二高频更新的表的索引数量要克制索引不是越多越好因为每次 INSERT 和 UPDATE 都要同步维护索引写事务的耗时和索引数量成正比。第三WHERE 条件里的列如果有枚举值比如状态字段不要单独建索引枚举值区分度太低优化器经常放弃索引反而全表扫描。一个具体的例子是库存流水表。某公司的进销存项目里流水表每天增长几万行查询按时间范围和商品 ID 两个条件。我给商品 ID 建了单列索引时间列不建索引因为查询总是按商品 ID 缩小范围后再扫时间商品 ID 索引能把锁范围限制到几十行这就够了。4.2 在线备份与崩溃恢复的取舍多用户环境下的备份比单机复杂得多。最直接的办法是停服拷贝文件但这在业务上往往不可接受。v7.90 的在线备份走的是备份 API逻辑是先记录当前日志序列号然后拷贝数据文件再拷贝增量的日志部分。备份出来的文件如果是中途状态打开时会自动回放日志到一致点。./db_backup --host 127.0.0.1 --port 54321 --output ./backup_2025_0612 --include-logs这个命令会在服务运行期间做一次一致性备份。--include-logs参数很关键它会把备份开始到结束之间产生的日志一并拷走否则恢复时只能恢复到一个小时前的状态中间数据全丢。实际操作时我会把备份脚本放到定时任务里凌晨跑一次全量白天每两小时跑一次增量。增量备份可以只拷贝日志文件前提是主数据文件没有损坏所以日志文件的保留策略至少要覆盖一个备份周期。崩溃恢复这块有个容易忽略的细节如果数据库目录里有残留的日志文件重启后服务端会自动进入回放流程这个时候千万不要手动删日志。某团队曾因日志目录太大运维自作主张删了一半历史日志结果数据库启动时发现日志断档数据一致到断档前一秒恢复失败还把现场搞坏了。恢复期间要做的是等待日志回放速度比正常写入慢大库可能要几分钟这期间客户端连上会显示“恢复中”状态码应用层要对此做提示而不是直接报错。另外一个安全习惯是定期执行完整性校验。v7.90 的 tools 目录下有个db_verify工具扫全库页面结构检查索引与表数据是否一致。我一般每周跑一次配合告警脚本输出里出现 PAGE_CHECKSUM_MISMATCH 就说明磁盘有问题或内存条有故障早发现能避免数据库整个损坏。这个话题单独讲可以写一整篇但核心就是别等到数据库打不开才想起来备份和校验。5. v7.90 多用户部署的六个典型坑与排查路径5.1 锁超时但日志里看不到死锁现象客户端频繁报锁超时但服务端日志里没有死锁记录只有普通的 lock wait 超时。原因最常见的不是死锁而是某个客户端开着事务不提交。v7.90 默认事务没有最长运行时间限制如果应用层忘记 commit 或 rollback事务就挂着一直占锁。排查时用db_admin --list-transactions能看到每个会话的持续时间定位到“僵尸事务”后从应用层杀掉对应连接。解决先解决眼前问题杀掉问题会话再改代码给所有事务提交点加 finally 确保关闭最后开数据库层的事务超时参数。我在实际项目中把事务空闲超时设为 5000 毫秒超过就按回滚处理虽然偶尔有误杀但比锁死整个表强。5.2 网络闪断后连接池里的连接全部失效现象交换机重启或者网线松动十几秒恢复后所有客户端报“connection reset”连接池重建后仍有部分请求失败。原因TCP 断开是感知不到的连接池里的连接在通信失败前看起来都是好的。v7.90 的客户端初始化握手带了一个会话 ID服务端重启或网络断开后会话 ID 失效旧连接发请求会直接失败。解决客户端接口加自动重连的逻辑在捕获到 connection reset 异常时先关闭旧连接再重连并重放事务。注意自动重连只对未开启事务的请求有意义已经开启事务的连接不能硬重放否则可能出现重复提交。正确做法是重连后先查询事务状态如果事务已回滚就重新执行整个业务逻辑如果事务状态未知就直接回滚并提示用户重新录入。5.3 数据库文件莫名变成只读现象服务端日志一切正常但客户端更新时报 no write permission文件权限也检查过没问题。原因这是文件型方案最容易踩的坑——主数据文件被备份进程或防病毒软件打开了而且是以独占方式打开的。Windows 上最常见的元凶是安全软件在后台扫描刚刚落地的备份文件锁住了源文件Linux 上则可能是日志清理脚本用chattr i设置了不可修改标志。解决先用文件检查命令确认是否有进程挂着句柄然后看文件属性标志位。备份时要通过数据库的备份 API 而不是直接拷贝文件这能避免备份工具自身触发文件锁。如果确认是安全软件就把数据库目录加到白名单里同时把备份文件输出到单独目录并加上排除规则。5.4 事务日志文件无限膨胀占满磁盘现象运行一个月后 data 目录比表实际大小还大几倍磁盘空间告急。原因日志不会被无限回收它要保证在数据文件被刷盘之前日志里的恢复信息是完整的。如果脏页迟迟没有落盘日志就会持续增长。解决先调检查点频率checkpoint_interval_min从默认 30 分钟改到 5 分钟让脏页更频繁写入主数据文件日志就能被截断。如果写入量实在太大检查是否有循环更新的任务——某个定时任务每秒钟更新同一批行这种热点写在日志上会产生大量冗余记录建议把这类任务改成批量合并后再写入。磁盘空间不足时要优先扩大磁盘而不是删日志日志对崩溃恢复来说是保命的。5.5 多线程客户端写入顺序错乱现象单线程写入没问题增加并发后出现部分数据互相覆盖且没有版本冲突异常。原因这通常是客户端代码把事务提交放在了线程池回调里但连接对象不是线程安全的。多个线程共用同一个连接会打乱请求序列v7.90 的客户端连接同一时间只允许一个事务出现交叉后协议状态机错乱服务端不会抛版本冲突因为接收到的请求本身就是乱序的。解决给每个线程分配独立连接或者用连接池并且保证一个连接同时只被一个线程使用。排查时看客户端日志里的发送时间戳同一个连接上如果两个线程分别在 0 毫秒和 15 毫秒发了请求就说明共用连接了。加个诊断工具在事务开始时记录线程 ID 和连接实例 ID能快速定位是哪段代码在共用。5.6 备份期间写入性能大幅下降现象每天凌晨备份时业务端明显卡顿查询延迟从几十毫秒涨到几百毫秒。原因备份过程中要保证数据一致性服务端会暂停脏页刷盘和日志截断这期间写请求要等更多日志落盘读请求也可能因为缓存页被备份扫描打穿而退化到磁盘读取。解决把备份时间挪到业务低谷同时给备份进程降低优先级。另外可以开启snapshot_io参数让备份进程直接读磁盘文件的静态快照不走共享缓存对业务的影响小很多。注意开启该参数后备份进程本身的 IO 会变高磁盘性能孱弱的机器要慎用。6. 压测验证多用户能力一个一小时内出结果的方案压测这件事最怕拍脑袋说“应该能行”。多用户数据库的验证标准只有一条在模拟真实业务比例的前提下并发达到预期峰值时事务成功率 100%p95 延迟不超阈值。我给过一个可用一小时的压测方案压测脚本按实际进销存业务的读写比例设计64% 是查询库存26% 是新增订单10% 是更新库存状态。import threading import time import random import dbclient_v790 as db stop_flag False success_count 0 fail_count 0 def worker(thread_id): global success_count, fail_count conn db.connect(127.0.0.1, 54321, userloadtest, passwordtest) while not stop_flag: action random.choices([query, insert, update], weights[64, 26, 10])[0] conn.begin() try: if action query: conn.execute(SELECT name, price FROM product WHERE id ?, (random.randint(1, 5000),)).fetchall() elif action insert: conn.execute(INSERT INTO orders(order_no, product_id, qty) VALUES (?, ?, ?), (f{thread_id}-{int(time.time()*1000)}, random.randint(1, 5000), random.randint(1, 20))) else: conn.execute(UPDATE stock SET qty qty - ? WHERE product_id ? AND qty ?, (1, random.randint(1, 5000), 1)) conn.commit() success_count 1 except Exception: fail_count 1 conn.rollback() conn.close() threads [threading.Thread(targetworker, args(i,)) for i in range(30)] for t in threads: t.start() time.sleep(600) stop_flag True for t in threads: t.join() print(fsuccess{success_count} fail{fail_count} rate{success_count / (success_count fail_count) * 100:.2f}%)压测跑完以后重点关注三个结果。成功率必须 100%只要有一个失败就要查原因多用户环境失败意味着用户数据录入失败。p95 延迟如果用客户端时间戳统计超过 300 毫秒就说明锁等待太频繁要么加索引、要么降并发数。第三个看的是 fail 分布是否集中在某类操作比如全落在 UPDATE 上那基本是锁超时参数设太小或者表锁退化。有一个调优技巧特别值得提更新类操作如果条件列区分度高比如精确到商品 ID尽量用等值条件而不是范围条件。范围条件的锁范围是一整段索引区间等值条件只锁单行对并发提升非常明显。某项目压测时 UPDATE 语句从qty 1改成qty 1 AND product_id ?之后锁冲突率直接降了一个数量级。压测结束要写一份运行基线记录内容包括峰值并发数、事务成功率、p95 延迟、CPU 占用、内存占用这五项。以后业务量增长或者代码改动后用同一套脚本重新压测对比基线就能判断是优化了还是劣化了。这个习惯是我踩过亏之后养成的——曾经有个项目上线三个月后突然变慢谁都不知道基线是多少翻了两天日志才定位到是查询条件里多了一个全表排序。从那以后我任何多用户方案落地第一件事就是压测出一份基线后面所有变更都拿它做参照。希望帮到你也欢迎把这些坑发给正在折腾多用户数据库的朋友少走一段弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。