如何在GUNDB中实现原子提交/原子事务操作?
GUN实现原子提交/事务的实操方案
GUN基于CRDT实现最终一致性,本身没有传统数据库的阻塞式强ACID事务,但可以通过内置的乐观事务机制,实现「多put操作整体生效、执行过程无中途插入变更」的需求,不用啃晦涩的官方文档,直接按下面的方法落地即可。
核心逻辑
GUN事务的底层是比较交换(CAS)+ 原子批量写:提交事务时会先校验你读取到的节点版本和当前线上版本是否一致,如果一致就把整组操作一次性写入,不一致就直接返回冲突,不会执行任何写入,从根本上避免中途插入变更导致的半成功问题。
落地步骤
- 第一步:一次性读取所有需要修改的关联节点的最新值和对应版本号,不要边读边写
- 第二步:在本地同步完成所有值的计算,生成最终要写入的全部内容,这一步不要插入任何异步逻辑,避免拿到的版本号过期
- 第三步:调用内置
transact方法批量提交所有put操作,同时传入之前读取到的版本号做冲突校验 - 第四步:如果提交返回版本冲突错误,重新执行「读状态→算新值→提交」的流程,直到提交成功即可
可直接复用的代码示例
// 封装原子提交方法,冲突自动重试 async function atomicUpdate() { // 1. 读取关联节点的最新值和版本 const userPath = 'user/123' const orderPath = 'order/456' const [userData, orderData] = await Promise.all([ new Promise(res => gun.get(userPath).once(val => res({ val, state: gun.state(gun.get(userPath)) }))), new Promise(res => gun.get(orderPath).once(val => res({ val, state: gun.state(gun.get(orderPath)) }))) ]) // 2. 本地计算新值,不要加异步逻辑 if (userData.val.balance < 100) throw new Error('余额不足') const newBalance = userData.val.balance - 100 const newOrderStatus = 'paid' // 3. 提交事务 return new Promise((res, rej) => { gun.transact( // 所有要原子执行的put全部放在这个回调里 (tx) => { tx.get(userPath).get('balance').put(newBalance) tx.get(orderPath).get('status').put(newOrderStatus) }, // 版本校验规则 { '#': { [userPath]: userData.state, [orderPath]: orderData.state } }, // 提交结果回调 (ack) => { if (ack.err) { // 版本冲突,直接重试 if (ack.err.includes('conflict')) return res(atomicUpdate()) return rej(ack.err) } res(ack.ok) } ) }) } // 调用即可,保证两个put要么全成要么全失败,不会被中途写入打断 atomicUpdate().then(() => console.log('事务提交成功'))
避坑提醒
- 不要在
transact的写入回调里加异步操作(比如接口请求、setTimeout、用户输入等待),会大幅提升版本冲突概率 - 需要保证原子性的所有put必须放在同一个
transact回调里,拆分多次调用无法实现原子性 - GUN的事务保证是最终原子性:分布式多节点场景下,整组操作会作为一个单元同步到所有节点,不会出现部分节点只应用一半操作的情况,但同步过程存在毫秒级延迟
- 单节点修改不需要手动传入版本校验参数,
transact内部会自动完成版本比对,只有多节点关联修改时需要手动配置校验规则
内容的提问来源于stack exchange,提问作者Raine Revere
相关产品推荐
相关产品推荐

