简介这份esqlite3 V1.1支持库与模块面向易语言开发中的SQLite数据库操作场景在1.0基础上补齐互斥体、聚合上下文、自动提交、进度处理等全局命令并为数据库与记录集增加事务锁状态、取行数、取数据库句柄等实用接口。资源共413个文件压缩包17.39MB核心文件类型涵盖易语言模块e、支持库文件fne/dll、C/C源码c/cpp/h以及SQL脚本与工程配置sln/vcxproj可满足二次开发、学习封装或直接调用等多种需求。目前已有341人学习。值得关注的是本版要求记录集手动关闭避免内部自动关闭带来的资源隐患数据库.开始事务()新增事务锁状态参数对多线程场景尤为重要附加数据库()支持密码参数取记录集多个()可执行分号分割的多条SQL并获取全部记录集这些改动使高并发读写和复杂查询的实现更稳妥、灵活。1. sqlite3易语言支持库和模块 V1.1多线程事务锁与加密这次升级改了什么做易语言数据库开发绕不开 sqlite3 这个嵌入式库而 esqlite3 这套支持库和模块最让人放心的地方是它把 SQLite3 源码和 AES 加密 codec 都编译进了底层不是简单的 ODBC 转发。V1.1 相对于 1.0 最值得关注的是多线程和事务控制新增 S3 互斥体进入/退出、聚合上下文给“开始事务()”加了事务锁状态参数同时把记录集改成必须手动关闭。对需要做桌面收银、单机 ERP、多线程采集入库的易语言项目来说这版直接影响数据库的并发稳定性和加密文件的可管理性。下面从文件结构开始把这些更新拆到能直接复现的程度。2. 从源码到支持库sqlite3.c、codec.c 与易语言模块的分工拿到资源包别急着在易语言里引用先把文件过一遍。这里不是黑匣子作者把 C 层源码一起放出来了意味着你可以自己编译也可以直接用预编译产物。2.1 文件清单哪些是 SQLite 本体哪些是加密外壳资源包里几个关键文件的角色如下文件名作用在 V1.1 里的定位configure.ac / Makefile.amautotools 构建脚本生成编译配置和 Makefile决定怎么把 C 源码编成动态库appveyor.bat / appveyor-test.batWindows 自动编译脚本在 AppVeyor 上自动编译并跑测试保证源码包可复现sqlite3.cSQLite3 官方合并源码数据库核心引擎所有 SQL 执行都走这里shell.cSQLite 官方命令行 shell调试时直接命令行操作数据库rijndael.cAES 算法实现为加密扩展提供 Rijndael/AES 块加密原语codec.cSQLite 页级加密扩展在读写数据库页时加解密这是“加密版 SQLite”的关键可以看到这个支持库不是纯易语言封装而是先改了 SQLite C 层再加上加密扩展最后用易语言模块把命令暴露出来。所以你在使用时如果遇到“数据库文件被加密”“附加库打不开”这类问题原因多半不在易语言代码而在 codec 层。2.2 编译与安装从 autotools 到易语言支持库引用如果你拿到的是源码包而不是成品 DLL常见做法是用 MinGW 或 MSVC 先编译再把编译出来的 sqlite3.dll 放到易语言程序目录。下面是一套最常见的编译流程./configure --enable-shared --prefix/usr/local make make install gcc -o sqlite3_shell sqlite3.c shell.c codec.c rijndael.c -lpthread -ldl ./sqlite3_shell test.db select sqlite_version();这里的--enable-shared是让 autotools 生成动态库而不是静态库prefix指定安装目录“最后一行 gcc 编译 shell 测试程序”是为了验证 codec 和 rijndael 能不能正常链接。如果你的 configure 脚本里没定义SQLITE_HAS_CODEC那加密命令会直接报“not compiled with encryption support”这也是很多编译翻车的起点。编译好的 DLL 放到 exe 同目录后在易语言 IDE 里打开“工具 → 支持库配置”勾选 esqlite3 支持库或者在模块引用里添加对应的 .ec / .e 文件。验证是否安装成功最直接的一段代码.版本 2 .支持库 esqlite3 .程序集 窗口程序集_启动窗口 .局部变量 数据库, zySqlite数据库 _启动窗口_创建完毕 数据库.打开 (“test.db”, , ) 数据库.执行 (“select sqlite_version()”) 输出调试文本 (数据库.取错误信息 ()) 数据库.关闭 ()这段代码的逻辑是以默认方式打开当前目录下的 test.db执行一条 SQL然后输出支持库内部的错误信息。第一个参数是数据库文件名第二个参数是打开参数第三个参数是密码V1.1 里密码通常和加密扩展配合使用没加密的库直接留空。如果 DLL 没放对位置打开()会返回假取错误信息会提示“无法定位DLL”或“找不到指定模块”先查文件路径。2.3 命令分层全局命令、数据库命令、记录集命令V1.1 的命令不是平铺的分成了三层全局命令S3互斥体进入、S3互斥体退出、S3聚合上下文、S3取数据库自上下文。这些不属于某个对象是支持库暴露的全局函数用来在多线程里保护共享连接以及在自定义函数回调里取上下文。zySqlite数据库命令打开、关闭、执行、开始事务、附加数据库、取记录集多个、繁忙超时、繁忙处理、取文件名、是否只读、取互斥体、是否自动提交、进度处理、取下一记录集、取总影响行。zySqlite记录集命令是否繁忙、是否只读、取数据库句柄、取行数、关闭等。V1.1 新增了前四项目的就是让你能更细地控制记录集生命周期。理解了分层再去翻模块里的命令列表就不会乱。全局命令管并发数据库命令管连接和事务记录集命令管查询结果。下面一章逐个拆新增命令。3. 新增命令逐个拆互斥体、聚合上下文与数据库状态查询这一章是对照 V1.1 更新说明逐条拆解。每条命令单独看都不难难的是知道它应该在什么场景里用。3.1 S3互斥体进入/退出多线程写库的护身符易语言做多线程时很多开发者习惯用一个全局数据库变量到处调用。问题是SQLite 默认的线程模式允许同一连接多个线程进入但同一个事务的写操作被打断容易出现 SQLITE_BUSY。V1.1 的互斥体命令本质是对支持库内部共享访问加临界区保护。典型用法是写操作前后各放一个.版本 2 .支持库 esqlite3 多线程任务里写数据 子程序 线程_写入数据 (参数) S3互斥体进入 () 这里只做数据库操作不要穿插耗时UI调用 数据库.开始事务 (1) 1 表示立即事务见第4章 数据库.执行 (“insert into log(msg) values(‘hello’)”, , , ) 数据库.提交 () S3互斥体退出 () 子程序结束这里的逻辑是进入互斥体之后同一时刻只允许一个线程执行临界区里的代码写事务不会被另一个线程的写操作打断。参数不用传互斥体是支持库内部的全局资源。需要注意进入和退出必须成对如果中间出现异常分支很容易造成互斥体泄漏其他线程全部卡死。所以在正式工程里我一般会把进入和退出放到一个类的两个方法里用对象生命周期保证必然成对。V1.1 还加了“取互斥体()”命令返回当前数据库连接关联的互斥体句柄。这个句柄可以传给其他线程实现跨线程手动锁定比全局命令更灵活。3.2 S3聚合上下文与取数据库自上下文自定义聚合函数的地基SQLite 支持自定义聚合函数比如实现一个自己的 group_concat。聚合函数需要 step 和 finalize 两个回调每一步都要把中间结果存到上下文里。V1.1 的“S3聚合上下文”就是拿到这个中间结果指针“S3取数据库自上下文”则是在回调里反查当前操作的数据库对象。回调模型大概是这样.版本 2 .支持库 esqlite3 step 回调每一行数据进入 子程序 聚合_累加 (上下文地址, 参数值) 局部变量 累计, 双精度小数型 累计 取聚合累计值 (S3聚合上下文 ()) 取聚合累计值_置 (累计 参数值) 子程序结束 finalize 回调返回最终结果 子程序 聚合_结束 (上下文地址) S3取数据库自上下文 () 演示在回调里反查当前数据库 返回 (取聚合累计值 (S3聚合上下文 ())) 子程序结束这段代码里实际命令名以你拿到的模块导出为准核心套路是每次回调都用S3聚合上下文()来读写中间状态而不是用易语言全局变量。原因很简单多线程下全局变量会被互相覆盖而聚合上下文是 SQLite 为每一条查询独立分配的天然隔离。S3取数据库自上下文()的用途是在聚合函数内部如果还要查其他表做关联统计可以用它取得当前数据库对象避免另行打开新连接。3.3 zySqlite数据库新增命令从繁忙超时到取下一记录集V1.1 在数据库对象上新增了八条命令我先给参数表再逐个说命令参数返回值说明繁忙超时毫秒数无或布尔设置 busy_timeout0 表示立即返回繁忙处理回调函数地址无自定义锁等待策略取文件名无文本当前数据库文件名是否只读无逻辑值当前连接是否只读取互斥体无整数/句柄当前连接关联的互斥体句柄是否自动提交无逻辑值假表示当前处于未提交事务中进度处理指令间隔, 回调地址无每执行 N 条虚拟机指令回调一次取下一记录集无逻辑值下一个结果集是否存在取总影响行无整数最近一次写操作影响的行数“繁忙超时”和“繁忙处理”都是解决 SQLITE_BUSY 的前者让支持库内部重试后者让你自己决定等待策略。“进度处理”适合长查询比如统计一个月订单量每执行到一定数量的 SQLite 内部指令就回调一次回调里可以更新进度条或标记取消。“取总影响行”和旧版“取受影响行”类似但在多事务场景下更明确它拿的是当前连接最近一次写操作的结果。取多个记录集的命令组合起来是这样.版本 2 .支持库 esqlite3 同时执行两条查询分别取两个记录集 数据库.取记录集多个 (“select * from user; select * from log”, 记录集, ) 循环判断首 (记录集.是否结束 () 假) 输出调试文本 (记录集.取字段值 (“name”)) 记录集.到下一条 () 循环结束 第一个记录集处理完取下一个 判断循环 (记录集.取下一记录集 ()) 输出调试文本 (记录集.取字段值 (“msg”)) 判断循环结束 V1.1 要求手动关闭 记录集.关闭 ()这段代码的逻辑是一次性把两条 SQL 的结果都拉出来先用循环处理第一个记录集再通过取下一记录集()跳到第二个。参数上取记录集多个()第一个参数是分号分隔的 SQL第二个参数是输出记录集对象第三个参数一般传空。这里最容易犯的错是把分号写在字符串字面量里具体避坑见第 5 章。4. 多线程事务与记录集生命周期手动关闭和事务锁状态参数V1.1 在事务和记录集上的改动是这次升级里最影响现有代码行为的两个点。升级后如果还按 1.0 的写法跑大概率会出现内存上涨或锁死。4.1 开始事务新增事务锁状态参数三个锁级别怎么选SQLite 的BEGIN有三种锁级别V1.1 把它们暴露成“开始事务()”的第二个参数参数值事务类型锁行为适用场景0DEFERRED读不加锁第一次写才升级为写锁单线程或只读1IMMEDIATE开始事务后立即申请保留写锁允许其他连接读多线程写推荐2EXCLUSIVE立即申请排他锁读写都不放行数据库迁移、重建为什么说这个参数在多线程里非常重要因为默认的 DEFERRED 事务两个线程可以同时读同一批数据然后都想升级成写锁互相等对方释放最后死锁。IMMEDIATE 是写事务一开始就抢写锁其他线程只能读不能写自然避免升级死锁。对应代码是.版本 2 .支持库 esqlite3 多线程写操作使用立即事务 数据库.开始事务 (1) 数据库.执行 (“update stock set qty qty - 1 where id 100”, , , ) 数据库.提交 ()参数 1 就是 IMMEDIATE。如果你用默认的 0在并发不高时也能跑但压测到多线程同时写就可能卡死。另外要注意事务内如果执行出错必须调“回滚()”否则锁不会被 SQLite 释放其他线程一直报“database is locked”。4.2 记录集必须手动关闭V1.1最关键的行为变更V1.1 更新说明里明确写了“记录集必须手动关闭任何内部方法都不再自动关闭”。这是最容易忽略的破坏性变更。原因并不复杂1.0 里“取记录集”用完后内部会顺手关闭但 V1.1 为了支持“取记录集多个()”和多结果集跳转记录集对象可能在取下一记录集之前还需要继续保留内部不能再自动关闭。这就把释放责任交给了调用者。正确的代码模式是这样数据库.取记录集 (“select * from user”, 记录集) 如果真 (记录集.是否结束 () 假) 这里处理数据 记录集.关闭 ()这里的关闭()区别于“销毁”“关闭”是释放底层 sqlite3_stmt 句柄对象本身可以再次使用。我自己的习惯是查询完立刻关闭绝不在下一次查询时才顺手关。旧版代码如果依赖自动关闭升级后短时间内看不出问题跑一个通宵任务就会暴露内存泄漏。4.3 附加数据库增加密码参数加密库怎么挂载加密是这套支持库的一个卖点。SQLite 原版不带加密esqlite3 通过 codec.c 在数据库页写入和读取时做 AES 加解密。V1.1 给“附加数据库()”增加了密码参数让你可以挂载另一个带独立密码的加密库。示例.版本 2 .支持库 esqlite3 数据库.打开 (“main.db”, “main”, “123456”) 数据库.附加数据库 (“sub.db”, “sub”, “654321”) 数据库.执行 (“select * from sub.t_user”, , , )这里的逻辑是主库用密码 123456 打开附加库 sub.db 用密码 654321 挂载挂载后通过别名 sub 访问它的表。第三个参数是新增的密码位如果附加的库没加密这个参数留空传空字符串即可。如果密码不对SQLite 会报“file is encrypted or is not a database”这时候先检查是不是把主库密码误填到附加库上了。5. 避坑手册esqlite3 V1.1 多线程场景下的五个常见翻车点V1.1 升级后我最常遇到的问题是下面这些每条都是实际跑过的血泪经验。5.1 记录集不关闭内存和文件锁一起爆**现象**程序循环执行查询内存持续上涨用一段时间后数据库文件无法删除甚至报“disk I/O error”。**原因**1.0 里内部自动关闭记录集V1.1 改成手动关闭后旧代码没有加“关闭()”每次查询都泄漏一个 sqlite3_stmt。查询少时看不出来循环几千次后就非常明显。**解决**把每一个取记录集操作都包上“关闭()”最稳妥的办法是修改取记录集的地方无论正常结束还是中途跳出都在最后强制关闭记录集。升级完成后全项目搜索“取记录集”逐一补关闭。5.2 事务锁状态传错多线程死锁**现象**两个线程同时写偶尔卡在“database is locked”重启程序又恢复过一段时间再次出现。**原因**两个线程都用了默认 DEFERRED 事务开始事务时不加锁执行到写语句时才申请升级互相持有共享锁等待对方释放形成死锁。**解决**所有写线程的“开始事务()”都传参数 1用 IMMEDIATE 事务提前占住写锁。再配合 S3互斥体进入/退出把临界区范围控制到最小基本能根除这个问题。5.3 附加带密码数据库参数传错导致“file is encrypted”现象“附加数据库()”调用成功但查询附加库的表时报错“file is encrypted or is not a database”。**原因**附加库是加密库但调用时没传密码或者把主库密码误填到附加库密码位置。codec 层解密失败后才返回这个错误。**解决**确认附加库当初是用哪个密码初始化的调用时严格按“文件名、别名、密码”顺序传参。主库和附加库密码可以不同但要分开传。如果附加的是普通库密码位留空不要传一个非空字符串。5.4 取记录集多个() 被分号打断结果集数量对不上**现象**用分号拼接多条 SQL 调用“取记录集多个()”返回的记录集数量比 SQL 条数少或者取字段时提示“no such column”。**原因**SQL 字符串里如果出现分号比如select a;b这种字符串字面量封装层如果简单按分号拆分就会把 SQL 切碎导致语句不完整。**解决**保持 SQL 语句里没有多余分号尤其是字符串字面量。如果确实要传特殊字符用参数绑定代替拼接。复杂场景建议分批次执行不要迷信一次调用跑完所有语句。5.5 繁忙超时和繁忙处理同时设后一个覆盖前一个**现象**先调用“繁忙超时(3000)”再调用“繁忙处理(回调地址)”但等待超时后仍然立即报错或者自定义回调根本没被触发。**原因**SQLite 底层 busy_timeout 和 busy_handler 是同一个机制的两面——设置后者会替换前者。封装层没有做共存处理后设置的就覆盖了先设置的。**解决**二选一。简单场景用“繁忙超时(毫秒)”设置后内部自动重试复杂场景用“繁忙处理”在回调里自己累计等待时间决定是继续还是放弃。不要两个都设。6. 进阶封装用 V1.1 组合一个多线程安全的数据访问类到这里命令和坑都清楚了剩下的是怎么把这些零散命令变成一个不容易用错的结构。6.1 封装思路互斥体 立即事务 强制关闭我习惯把数据库操作封装成类对外只暴露“初始化、写数据、查询数据、释放记录集”四个方法。内部用互斥体保护写操作写事务全部用 IMMEDIATE记录集在回调结束后立刻关闭。业务代码不需要关心锁细节。.程序集 类_安全数据库 .局部变量 数据库, zySqlite数据库 子程序 写数据 (SQL语句) S3互斥体进入 () 数据库.开始事务 (1) 数据库.执行 (SQL语句, , , ) 数据库.提交 () S3互斥体退出 () 子程序 查询数据 (SQL语句, 记录集) 数据库.取记录集 (SQL语句, 记录集) 调用方处理完后必须调用释放记录集 子程序 释放记录集 (记录集) 记录集.关闭 ()这相当于把第 4 章的要点固化到代码结构里互斥体保证同一时间只有一个线程在写立即事务避免升级死锁强制关闭避免泄漏。6.2 压测验证多线程写库不报错、内存平稳封装完要压测不然心里没底。我一般开四个线程每个线程循环写 200 条记录然后观察两点是否出现“database is locked”进程内存是否持续增长。测试片段计次循环首 (200, i) 类数据库.写数据 (“insert into load_test(id, tag) values(” 到文本 (i) “, ‘sync’)”) 计次循环尾 ()跑完后检查表里记录数是否等于理论值。如果出现锁错误优先检查“繁忙超时(3000)”有没有在最开始设置如果内存暴涨优先检查记录集关闭。从那以后我每次升级支持库都会强制走一遍这个流程先改记录集关闭再确认事务锁状态最后压测半天。这套组合拳打下来易语言里用 SQLite 的玄学问题基本绝迹。希望帮到你。本文还有配套的精品资源点击获取
