简介:Absolute Database Multi User Source v7.90 是一套面向 Delphi 开发者的多用户数据库组件完整源码包,适合需要在桌面或 C/S 架构应用中嵌入轻量级数据库引擎的中高级程序员,用于替代 BDE 或简化数据持久层实现。压缩包共 567 个文件,约 6.94MB,以 sql、sqm 脚本与 pas、dpk、dfm 等 Delphi 源码和包文件为主,同时包含 c、cpp、h、hpp、obj 等 C/C++ 侧接口与编译产物,以及 res、ico、dcr 等资源文件,并附带 bdsproj、dproj、bpk、sln、vcxproj 等多版本工程配置,覆盖 Delphi 与 C++Builder 多代 IDE 的编译需求。资源内含全部源码,读者可深入研究多用户并发控制、事务处理与 SQL 解析等核心机制,并依据 bat 脚本与工程文件快速完成环境搭建与二次开发。目前已有 262 人学习下载,适合希望掌握嵌入式数据库底层实现或进行组件定制的开发者参考。
1. 多用户源码库 v7.90:为什么单机版数据库一进团队就翻车
单机跑得好好的数据库应用,一旦交给三个人同时用,十有八九会在两周内出现「数据对不上」的投诉。Absolute_Database_Multi_User_Source_v7.90 这个标题指向的,就是这类场景的解法:一套支持多用户并发访问的数据库源码,版本号 v7.90 意味着它已经迭代过相当多轮,不是玩具项目。它要解决的核心问题很具体——多个客户端同时读写同一份数据时,如何保证不丢更新、不读到脏数据、不因为一个人卡住让所有人排队。适合谁?适合手里已经有一个单用户版数据库工具、现在要把它改造成团队可用版本的开发者;也适合想研究多用户并发控制怎么落地、不想从零造轮子的工程师。这一章先把「多用户」这三个字背后的真实需求拆开,后面几章再落到源码结构、并发参数和踩坑记录上。
2. 多用户源码的骨架:从连接池到事务隔离的四个关键层
2.1 为什么单用户版直接加锁会把自己锁死
很多单用户数据库的源码结构是「一个全局文件句柄 + 顺序读写」。这种结构在单人使用时没问题,因为不存在竞争。但多用户场景下,最直觉的改法是「读写前加一把大锁」,结果就是:A 用户在做一个耗时 3 秒的批量导入,B 用户连查询都被阻塞,C 用户直接超时断开。这不是多用户,这是排队机。
Absolute_Database_Multi_User_Source_v7.90 这类源码通常不会用全局锁,而是把并发控制拆成四层:连接层、会话层、事务层、存储层。连接层负责接纳多个客户端连接,会话层维护每个用户的临时状态(比如未提交的修改),事务层决定哪些操作可以并行、哪些必须串行,存储层才真正碰数据文件。这四层里,事务层是 v7.90 这种版本号最值得看的地方——它一般会实现某种多版本并发控制(MVCC)或者行级锁,而不是表级锁。
我一般会先看源码里事务开始和提交的函数签名。如果begin_transaction只接受一个全局参数,那大概率还是粗粒度锁;如果它接受用户 ID 或者会话 ID,并且有独立的版本号或时间戳,那才是真正的多用户设计。这个判断方法不需要读完整个源码,十分钟就能定性。
2.2 连接池参数怎么设:三个数字决定并发上限
多用户源码里,连接池是最容易被忽视但最影响体验的模块。连接池太小,用户一多就报「连接不可用」;太大,数据库文件句柄和内存会被吃光。v7.90 这类源码通常会暴露三个关键参数:最大连接数、空闲超时、获取连接的超时时间。
下面是一段典型的连接池配置代码,语言用 Python 示意,因为多数数据库源码的配置层会用脚本或配置文件暴露:
# 连接池配置示例:对应 v7.90 源码中 pool_config 段 POOL_CONFIG = { "max_connections": 32, # 最大并发连接数,不是越大越好 "idle_timeout": 300, # 空闲连接 5 分钟后回收,单位秒 "acquire_timeout": 10, # 获取连接最多等 10 秒,超时抛异常 "min_cached": 4, # 最少保留 4 个热连接,避免反复创建 } # 初始化连接池 pool = ConnectionPool( db_path="./data/absolute.db", config=POOL_CONFIG, user_isolation=True # 关键:开启用户级隔离,每个会话独立事务上下文 )逻辑说明:max_connections设为 32 是经验值,对应大约 20 个活跃用户加一些后台任务。idle_timeout不能设太短,否则用户思考几秒再操作就要重连;也不能太长,否则闲置连接占着文件句柄。acquire_timeout是保护机制,防止某个慢查询把连接池耗尽后,其他用户无限等待。user_isolation这个开关在多用户源码里通常是默认关闭的,必须显式打开,否则不同用户会共享事务上下文,出现「我看到了别人未提交的数据」这种玄学问题。
参数怎么改:如果团队只有 5 个人,max_connections设 16 就够;如果经常有批量任务,把acquire_timeout调到 30 秒,给慢操作留余地。改完不要只看日志,要用并发测试脚本压一下,看连接池的等待队列长度。
2.3 事务隔离级别在源码里对应哪几行
多用户数据库最核心的选型是事务隔离级别。v7.90 这类源码一般会支持读未提交、读已提交、可重复读、串行化四种,但默认值通常是「读已提交」。为什么?因为读未提交会脏读,串行化性能太差,可重复读在部分实现里会加间隙锁导致死锁概率上升。
在源码里找隔离级别的实现,一般看两个地方:一是事务结构体里有没有snapshot_version字段,二是读操作前有没有调用check_visibility之类的函数。如果有快照版本,说明用的是 MVCC,读操作不会阻塞写操作,这是多用户场景最理想的。如果只有锁标志位,那读和写会互相阻塞,并发能力有限。
我一般会做一个简单测试:开两个会话,A 开始事务但不提交,B 去读同一行。如果 B 能立刻读到旧值,说明是 MVCC;如果 B 卡住,说明是锁。这个测试比看文档快,而且能直接验证源码行为。
2.4 用户权限表的设计:别让所有人都是管理员
多用户源码里,权限表往往是最先被写死、最后被重构的部分。v7.90 这种版本号意味着权限模型应该已经比较成熟,通常会有用户表、角色表、权限表三张核心表。常见做法是:用户表存账号和密码哈希,角色表存角色名,权限表存「角色-资源-操作」三元组。
下面是一段建表 SQL,对应多用户源码初始化时执行的权限模块:
-- 用户表:存账号基础信息 CREATE TABLE users ( user_id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 角色表:一个用户可以有多个角色 CREATE TABLE roles ( role_id INTEGER PRIMARY KEY, role_name TEXT UNIQUE NOT NULL ); -- 权限表:角色对某张表或某个操作的许可 CREATE TABLE permissions ( perm_id INTEGER PRIMARY KEY, role_id INTEGER NOT NULL, resource TEXT NOT NULL, -- 例如 "orders" 或 "orders.amount" action TEXT NOT NULL, -- "read" / "write" / "delete" FOREIGN KEY (role_id) REFERENCES roles(role_id) ); -- 用户角色关联 CREATE TABLE user_roles ( user_id INTEGER NOT NULL, role_id INTEGER NOT NULL, PRIMARY KEY (user_id, role_id) );逻辑说明:resource字段用点号分隔可以做到列级权限,比如只允许某角色读orders表但不能读orders.amount。action用字符串而不是枚举,是为了方便扩展新操作类型。参数上,password_hash不要用明文,v7.90 源码里一般会调用 bcrypt 或 argon2,如果发现是 MD5,那这个版本的安全性需要打问号。
注意:权限检查不要只放在应用层,源码里的存储层也要做一次校验。我见过太多案例,应用层拦住了,但直接调底层 API 就能绕过权限,多用户环境下这是致命的。
3. 把 v7.90 跑起来:从源码编译到第一个多用户测试
3.1 编译前先确认这三个依赖版本
Absolute_Database_Multi_User_Source_v7.90 这类源码通常依赖几个基础库:并发运行时、加密库、文件锁库。版本不匹配是编译失败的头号原因。常见做法是先看源码根目录的依赖声明文件,然后逐项核对。
| 依赖项 | 常见要求 | 检查命令 | 不匹配的后果 |
|---|---|---|---|
| 并发运行时 | 支持协程或线程池 | runtime --version | 连接池初始化失败 |
| 加密库 | 支持 bcrypt/argon2 | crypto --version | 用户密码无法哈希 |
| 文件锁库 | 支持跨进程锁 | locklib --version | 多进程写入时数据损坏 |
| 构建工具 | 支持 C++17 或同等 | build --version | 编译报语法错误 |
这张表不是让你照抄版本号,而是让你知道每个依赖对应哪个功能模块。缺并发运行时,多用户就是空谈;缺文件锁,两个进程同时写同一个文件会直接损坏数据。
3.2 编译命令与首次初始化
依赖确认后,编译本身通常只有两三条命令。下面用 bash 示意:
# 进入源码目录 cd absolute-database-multi-user-src # 配置构建,开启多用户支持 ./configure --enable-multi-user --with-pool-size=32 --prefix=/opt/absdb # 编译并安装 make -j4 && make install # 初始化数据库实例,指定管理员账号 /opt/absdb/bin/absdb_init --db-path=/data/absdb --admin-user=admin逻辑说明:--enable-multi-user是核心开关,不开的话编译出来还是单用户版。--with-pool-size=32把连接池上限写进编译期默认值,运行期还能改,但编译期设好可以避免忘记。make -j4用 4 个并行任务编译,机器核多可以调大。absdb_init会创建数据目录、权限表和初始管理员,--admin-user不指定的话可能生成随机账号,后面登录会麻烦。
参数怎么改:如果机器内存小于 4GB,--with-pool-size降到 16;如果数据目录在机械硬盘上,初始化时加--sync=normal降低写入频率,但断电可能丢最后几秒数据。
3.3 用两个客户端验证并发读写
编译安装完,别急着接业务,先用两个客户端做最小并发测试。下面是一段 Python 测试脚本:
import threading from absdb import connect def writer(user, value): conn = connect(user=user, db="testdb") conn.begin() conn.execute("UPDATE counter SET val = val + 1") conn.commit() conn.close() def reader(user): conn = connect(user=user, db="testdb") conn.begin() result = conn.execute("SELECT val FROM counter") print(f"{user} 读到: {result.fetchone()}") conn.commit() conn.close() # 初始化计数器 init = connect(user="admin", db="testdb") init.execute("CREATE TABLE IF NOT EXISTS counter (val INT)") init.execute("INSERT INTO counter VALUES (0)") init.commit() # 两个写线程同时加一 t1 = threading.Thread(target=writer, args=("user_a", 1)) t2 = threading.Thread(target=writer, args=("user_b", 1)) t1.start(); t2.start() t1.join(); t2.join() # 读最终值 reader("admin")逻辑说明:两个写线程同时执行val = val + 1,如果最终读到 2,说明行级锁或 MVCC 生效;如果读到 1,说明发生了丢失更新,多用户并发控制有问题。这个测试比任何文档都直接。参数上,connect的user参数必须不同,否则测不出用户隔离;begin和commit必须成对,漏掉 commit 会导致锁不释放,后续操作全部超时。
注意:测试前把counter表清空或重建,避免历史数据干扰判断。如果读到 2 但偶尔读到 1,说明隔离级别不够,需要把默认级别调到可重复读或串行化。
4. 多用户并发下的避坑记录:五个血泪教训
4.1 现象:用户 A 提交后,用户 B 看不到新数据
原因:连接池复用了旧连接,而旧连接持有的是旧快照。v7.90 源码里如果user_isolation没开,或者连接归还时没有重置事务上下文,就会出这个问题。
解决:在连接归还逻辑里强制调用reset_session(),并且把user_isolation设为 True。如果源码里没有这个函数,就在应用层每次获取连接后手动执行一次SET SESSION SNAPSHOT = CURRENT。
4.2 现象:并发写入时偶尔报「文件锁超时」
原因:两个进程同时写同一个数据文件,文件锁库的等待时间设得太短。默认可能是 1 秒,批量导入时根本不够。
解决:把文件锁等待时间调到 10 秒以上,并且在应用层做重试。重试次数不要超过 3 次,否则会放大延迟。更好的做法是让写操作排队,而不是让它们竞争锁。
4.3 现象:某个用户断开后,其他人全部卡住
原因:断开连接时没有释放事务锁。多用户源码里,连接异常关闭时应该触发回滚和锁释放,但如果源码的异常处理不完整,锁就会一直挂着。
解决:在连接层加心跳检测,超时未心跳的连接强制回收并回滚事务。同时检查源码里on_disconnect回调是否调用了rollback_if_needed。
4.4 现象:权限表改了,但已登录用户还是旧权限
原因:权限缓存在会话层,修改权限表后没有通知活跃会话刷新。这是多用户系统的经典问题。
解决:权限变更后,要么让相关用户重新登录,要么在源码里加一个权限版本号,每次检查权限时对比版本号,不一致就重新加载。我一般选后者,体验更好,但需要改源码。
4.5 现象:批量导入时内存暴涨,最后 OOM
原因:多用户源码为了支持回滚,会把未提交的修改缓存在内存里。批量导入几万行,内存直接吃满。
解决:把大批量操作拆成小事务,每 1000 行提交一次。同时检查源码里有没有max_transaction_size参数,有的话设成 5000 左右。不要在一个事务里做十万行插入,那不是多用户设计的目标场景。
5. 进阶:用版本号做乐观并发控制,把锁竞争降到最低
多用户源码跑到后面,最影响吞吐的不是连接数,而是锁竞争。v7.90 这类版本通常会提供乐观并发控制的选项,核心思路是:读的时候不加锁,写的时候检查版本号,如果版本变了就重试。这比悲观锁适合读多写少的场景。
具体做法是在表里加一个version字段,每次更新时version = version + 1,并且更新语句带上旧版本号作为条件。下面是一段示意代码:
def update_with_optimistic_lock(conn, row_id, new_value, max_retry=3): for attempt in range(max_retry): # 读当前值和版本号 row = conn.execute( "SELECT val, version FROM items WHERE id = ?", (row_id,) ).fetchone() old_val, old_version = row # 带版本号条件更新 affected = conn.execute( "UPDATE items SET val = ?, version = version + 1 " "WHERE id = ? AND version = ?", (new_value, row_id, old_version) ).rowcount if affected == 1: conn.commit() return True # 版本冲突,重试 conn.rollback() return False逻辑说明:WHERE version = ?是乐观锁的关键,如果另一个用户在这期间改了同一行,affected会是 0,当前事务回滚后重试。max_retry=3是经验值,超过 3 次说明冲突太频繁,应该考虑换悲观锁或者调整业务逻辑。参数上,version字段用整数自增,不要用时间戳,时间戳在并发下可能重复。
验证方法:开两个线程同时更新同一行,观察是否一个成功一个重试。如果两个都成功但最终值不对,说明版本号条件没生效,检查 SQL 里有没有漏掉AND version = ?。
我自己的习惯是:读多写少的表用乐观锁,写多的表用行级悲观锁,混合场景就在源码里按表配置。这个方案值不值得做?如果你的团队超过 5 个人同时用,或者有自动化任务和人工操作混跑,那多用户源码不是可选项,是必选项。从单用户改多用户,最省力的路径就是找一套像 v7.90 这样已经处理好连接池、事务隔离和权限的源码,把精力放在业务逻辑上,而不是重新发明并发控制。希望帮到你。
本文还有配套的精品资源,点击获取