异步数据库插入代码多线程场景下数据混淆问题排查与修复咨询
问题场景
我写了一段Dart代码,用来根据传入的JSON生成SQL并插入数据库,核心代码如下:
Future<dbEnums> matchAndInsert(String section, Map jsonMap) async { tableToInsert = "notifications"; try { db.saveOriginalJson(jsonMap); await db.KVToTableInsert(tableToInsert, jsonMap); // 省略剩余代码... } catch (e) { // 异常处理... } }
其中KVToTableInsert内部会存储一些临时数据(比如从JSON中提取的嵌套表名称)。
对应的请求处理器是这样的:
app.post('/foo', (req, res) async { dbEnums insertResult = await matchQuery.foo("tablename", req.body); // 响应处理... });
单线程调用时一切正常,但用多线程解析器(涉及不同根表和嵌套表)发送请求时程序崩溃了——错误显示尝试向table2插入数据时,部分字段却来自table1。我怀疑是异步使用不当导致了数据重叠,是不是KVToTableInsert还没执行完,saveOriginalJson里的字段就被覆盖了?该怎么修复db.saveOriginalJson(jsonMap); await db.KVToTableInsert(tableToInsert, jsonMap);这部分代码?
问题分析
你猜的没错,核心问题就是共享状态的并发冲突。看起来你的db实例是全局共享的,而且KVToTableInsert内部维护的临时数据(比如嵌套表名称)是实例级的共享变量。当多个请求同时进入异步流程时:
- 请求A调用
saveOriginalJson把自己的JSON存进db的共享字段 - 请求A还没跑完
KVToTableInsert,请求B又调用saveOriginalJson覆盖了这个共享字段 - 等请求A的
KVToTableInsert继续执行时,它拿到的已经是请求B的JSON数据了,自然会出现跨表字段混乱的情况
简单说就是异步操作没做好隔离,多个请求在同一个db实例上抢用共享状态,导致数据串了。
修复方案
1. 给数据库操作加锁(快速临时修复)
如果不想大改结构,可以在db实例里加一个互斥锁,保证同一时间只有一个请求执行saveOriginalJson和KVToTableInsert的组合操作:
// 在你的db类里添加一个锁对象 final _lock = Lock(); // 修改原来的调用逻辑,把两个操作放进锁里 Future<void> safeSaveAndInsert(String table, Map json) async { await _lock.synchronized(() async { saveOriginalJson(json); await KVToTableInsert(table, json); }); }
然后在matchAndInsert里调用这个安全方法:
Future<dbEnums> matchAndInsert(String section, Map jsonMap) async { tableToInsert = "notifications"; try { await db.safeSaveAndInsert(tableToInsert, jsonMap); // 剩余代码... } catch (e) { // 异常处理... } }
这个方法能快速解决并发冲突,但缺点是会把所有请求串行化,高并发场景下性能会受影响。
2. 重构DB类,去掉共享状态(推荐长期方案)
更好的做法是让KVToTableInsert不再依赖实例级的共享临时数据,而是把需要的临时数据作为局部变量或者参数传递:
- 把
KVToTableInsert内部用到的临时数据(比如嵌套表名称)改成方法内的局部变量,不要存在db实例的成员变量里 - 如果确实需要在
saveOriginalJson和KVToTableInsert之间传递数据,把这些数据封装成一个单独的上下文对象,而不是存在db实例里
比如重构后的KVToTableInsert可以改成这样:
Future<void> KVToTableInsert(String table, Map json) async { // 把原来的共享临时变量改成局部变量 final nestedTables = <String>[]; // 提取嵌套表名称的逻辑,直接存在nestedTables里,不碰实例变量 // ... 原来的插入逻辑,用局部的nestedTables处理 }
这样每个请求的KVToTableInsert都是独立的,完全不会互相干扰,从根源上解决并发问题。
3. 为每个请求创建独立DB实例(备选方案)
如果你的数据库驱动支持,也可以为每个请求创建一个独立的db实例,这样每个请求的操作都是完全隔离的,不会有共享状态冲突。不过这个方案要注意数据库连接池的问题,别创建太多连接导致资源耗尽。
内容的提问来源于stack exchange,提问作者Dmitry Bubnenkov

