简介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 与 CBuilder 多代 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, configPOOL_CONFIG, user_isolationTrue # 关键开启用户级隔离每个会话独立事务上下文 )逻辑说明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/argon2crypto --version用户密码无法哈希文件锁库支持跨进程锁locklib --version多进程写入时数据损坏构建工具支持 C17 或同等build --version编译报语法错误这张表不是让你照抄版本号而是让你知道每个依赖对应哪个功能模块。缺并发运行时多用户就是空谈缺文件锁两个进程同时写同一个文件会直接损坏数据。3.2 编译命令与首次初始化依赖确认后编译本身通常只有两三条命令。下面用 bash 示意# 进入源码目录 cd absolute-database-multi-user-src # 配置构建开启多用户支持 ./configure --enable-multi-user --with-pool-size32 --prefix/opt/absdb # 编译并安装 make -j4 make install # 初始化数据库实例指定管理员账号 /opt/absdb/bin/absdb_init --db-path/data/absdb --admin-useradmin逻辑说明--enable-multi-user是核心开关不开的话编译出来还是单用户版。--with-pool-size32把连接池上限写进编译期默认值运行期还能改但编译期设好可以避免忘记。make -j4用 4 个并行任务编译机器核多可以调大。absdb_init会创建数据目录、权限表和初始管理员--admin-user不指定的话可能生成随机账号后面登录会麻烦。参数怎么改如果机器内存小于 4GB--with-pool-size降到 16如果数据目录在机械硬盘上初始化时加--syncnormal降低写入频率但断电可能丢最后几秒数据。3.3 用两个客户端验证并发读写编译安装完别急着接业务先用两个客户端做最小并发测试。下面是一段 Python 测试脚本import threading from absdb import connect def writer(user, value): conn connect(useruser, dbtestdb) conn.begin() conn.execute(UPDATE counter SET val val 1) conn.commit() conn.close() def reader(user): conn connect(useruser, dbtestdb) conn.begin() result conn.execute(SELECT val FROM counter) print(f{user} 读到: {result.fetchone()}) conn.commit() conn.close() # 初始化计数器 init connect(useradmin, dbtestdb) init.execute(CREATE TABLE IF NOT EXISTS counter (val INT)) init.execute(INSERT INTO counter VALUES (0)) init.commit() # 两个写线程同时加一 t1 threading.Thread(targetwriter, args(user_a, 1)) t2 threading.Thread(targetwriter, 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_retry3): 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_retry3是经验值超过 3 次说明冲突太频繁应该考虑换悲观锁或者调整业务逻辑。参数上version字段用整数自增不要用时间戳时间戳在并发下可能重复。验证方法开两个线程同时更新同一行观察是否一个成功一个重试。如果两个都成功但最终值不对说明版本号条件没生效检查 SQL 里有没有漏掉AND version ?。我自己的习惯是读多写少的表用乐观锁写多的表用行级悲观锁混合场景就在源码里按表配置。这个方案值不值得做如果你的团队超过 5 个人同时用或者有自动化任务和人工操作混跑那多用户源码不是可选项是必选项。从单用户改多用户最省力的路径就是找一套像 v7.90 这样已经处理好连接池、事务隔离和权限的源码把精力放在业务逻辑上而不是重新发明并发控制。希望帮到你。本文还有配套的精品资源点击获取
