用【库存】看懂云开发数据库事务
在正常使用数据库 (CRUD) 的情况下,这些操作都会顺利进行所有数据都会被成功更新,由于某些特定的业务场景,需要进行一系列的操作,在这过程中必须保证每一步的操作都正常执行,如果任何一个环节出了差错,比如更新库存信息发生异常,这终将会导致数据库的信息混乱而不可预测,数据库事务正是用来保证这种一系列操作的稳定性技术
什么是事务?
数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成,这些操作要么全部执行, 要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务的特性
ACID,指数据库事务正确执行的四个主要特性的缩写,一个事务,必需要具有这四种基本特性,否则在事务过程当中无法保证数据的正确性
1: 原子性(Atomicity)
指的是一个事务内所有操作共同组成一个原子包,要么全部成功,要么全部失败。
假如在数据库中对一个属性进行了更新,但是执行到一半的时候出现了异常,这样就可能使得操作后的数据与我们预期的数据不同,所以原子性要求你这个方法要么全部执行成功,要么全部失败
2: 一致性(Consistency)
指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。
在原子性中规定方法中的操作都执行或者都不执行,但并没有说要所有操作一起执行,所以操作的执行也是有先后顺序的,那我们要是在执行一半时查询数据库,那我们会得到中间的更新的属性?一致性规定提交前后只存在两个状态,提交前的状态和提交后的状态
3: 隔离性(Isolation)
指的是数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
多个事务可能操作同一数据库资源,不同的事务为了保证隔离性,如果没有隔离会造成几种问题
- 事务 A 读到事务 B 修改却未提交的数据,事务 B 回滚数据修改操作,导致了事务 A 获得数据是脏数据
- 事务 A 先读取数据,事务 B 对数据进行修改,事务 B 再一次读取该行数据时就会造成前后两次读取结果不一致
- 事务 A 读取数据,事务 B 对其进行操作时,当事务 A 重新读取该段数据时会造成前后两次查询的数据不一致的现象
目前云开发数据库使用的是快照隔离,具体将在下面进行介绍
4: 持久性(Durability)
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
如果没有持久性的特性,一旦数据库出现异常,数据将会丢失
拥有持久性事务一旦提交后,数据库中的数据必须被永久的保存下来,即使服务器系统崩溃或服务器宕机等故障,只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。
云开发数据库事务
介绍
云开发数据库本身有提供(如 inc、mul、addToSet)等原子性操作符号和嵌套记录的数据结构设计, 如跨多个记录或跨多集合的原子操作时,可以使用云数据库事务能力。
隔离性
云开发数据库事务过程中采用的快照隔离级别(snapshot),在事务期间,读操作返回的是对象的快照,而非实际数据,事务期间写操作执行时:
- 改变快照,保证接下来的读的一致性;
- 给对象加上事务锁
事务锁
数据对象存在事务锁对数据写入的影响:
- 其它事务的写入会直接失败;
- 普通的更新操作会被阻塞,直到事务锁释放或者超时事务提交后,操作完毕的快照会被原子性地写入数据库中
单记录操作
云开发数据库事务中不支持批量操作,只支持单记录操作比如(collection.doc, collection.add),单记录操作可避免大量锁冲突、保证运行效率,并且大多数情况下单记录操作足够满足需求,因为在事务中是可以对多个单个记录进行操作的,也就是可以在一个事务中同时对集合 A 的记录 x 和 y 两个记录操作、又对集合 B 的记录 z 操作,接下来会通过小示例来进行演示。
事务 API
云开发数据库事务提供两种操作风格的接口,一个是简易的、带有冲突自动重试的 runTransaction 接口,一个是流程自定义控制的 startTransaction 接口。
使用小示例
假设有以下场景:
某仓库有 1000 箱医用口罩,A 医院需要 800 箱、B 医院需要 300 箱并提交申请,仓库的管理模式是先收到提交申请在进行库存商品确认完毕后,进行领用。
在无事务的情况下伪代码自上而下执行
const cloud = require('wx-server-sdk')
cloud.init({env: cloud.DYNAMIC_CURRENT_ENV})
const db = cloud.database()
const _ = db.command
exports.main = async (event, context) => {
// 医院
await db.collection('resource').doc('A'||'B')
.update({
data: {resource: _.inc(-800||-300)
},
})
// 仓库
await db.collection('store').doc('store')
.update({
data: {resource: _.inc(+800||+300),
},
})
}
// 判断是否满足要求
if('仓库库存' >'领用数量'){await db.collection('store').doc('store')
.update({
data: {count:_inc(-800||-300),
},
})
}eles{'回退的业务逻辑'}
}
根据以上的代码执行结果来看:
- A/ B 医院提交了领用口罩的申请;
- 仓库接收了 B 医院提交的申请;
- 判断是否符合数量要求
执行到 3 时候发现仓库库存,并不能满足医院的领取要求时, 需要将提交申请退还给医院, 并处理一些退回的逻辑。
该情况下需要处理操作量大、复杂度高、在高并发的执行情况下会导致一些具体的操作没有完成比如:
- 医院提交了申请,仓库并没有收到;
- 医院提交了申请,仓库收到申请,并没有执行发放,也没有退还给医院;
事务的情况下伪代码自上而下执行
const cloud = require('wx-server-sdk')
cloud.init({env: cloud.DYNAMIC_CURRENT_ENV})
const db = cloud.database({throwOnNotFound: false,})
const _ = db.command
exports.main = async (event) => {
try {
const result = await db.runTransaction(async transaction => {const resource = await transaction.collection('resource').doc('A'||'B').get()
const store = await transaction.collection('store').doc('store').get()
const updateResource = await transaction.collection('resource').doc('A'||'B').update({
data: {resource: _.inc(-800||-300)
}
})
const updateStoreResource = await transaction.collection('store').doc('store').update({
data: {resource: _.inc(+800||+300),
}
})
if(store.data.count > 800||300){const updateStoreCount = await transaction.collection('store').doc('store').update({
data: {count:_inc(-800||-300),
}
})
// 会作为 runTransaction resolve 的结果返回
return {resourceAccount: resource.data.count + 800||300,}
}else{
// 会作为 runTransaction reject 的结果出去
await transaction.rollback('领取失败')
}
})
return {
success: true,
resourceAccount: result.resourceAccount,
}
} catch (e) {console.error(`transaction error`, e)
return {
success: false,
error: e
}
}
}
根据以上的代码执行结果来看:
1. 首先读取了 A / B 医院与仓库的记录快照;
2. 医院提交申请,减少对应的数量;
3. 仓库接收到医院的提交申请;
4. 判断仓库中的数量是否满足本次领取的数量;
执行到 4 时候发现仓库库存,并不能满足医院的领取要求时, 事务会将所有更改的记录还原到读取记录快照时的数据,也就是说这些执行步骤 要不就都成功,要不就都失败,数据回滚,不需要过多的回退逻辑
未使用事务 VS 使用事务
未使用事务
- 由于操作量大,复杂度高,在加上出现高并发的情况就会有数据不一致的情况出现;
- 回退逻辑复杂;
使用事务
- 事务由一个有限的数据库操作序列构成,这些操作要么全部执行, 要么全部不执行,保证了数据一致性;
- 在执行事务之后保留了数据对象的快照,执行中出现任何问题可直接回滚;
总结
在使用云开发数据库中,如果仅仅是涉及单记录的修改,完全可以使用如 inc、mul、addToSet)等原子性操作符号,涉及到跨集合以及多个记录同时修改并需要保证一致性的情况,那事务功能将是最好的选择。