1.数据库事务
在执行SQL语句时,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分。例如,一个转账操作:
从id=1的账户给id=2的账户转账100元
-- 第一步:将id=1的A账户余额减去100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 第二步:将id=2的B账户余额加上100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
这两条SQL语句必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。
这种把多条语句作为一个整体进行操作的功能,被称为数据库事务
。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
可见,数据库事务具有ACID这4个特性(参考地址):
- A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
- C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
- I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
- D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。
对于MySQL数据库,要手动把多条SQL语句作为一个事务执行,可以使用BEGIN
开启一个事务,使用COMMIT
提交一个事务,这种事务被称为显式事务,例如,把上述的转账操作作为一个显式事务:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
而对于nodejs使用knex框架,在不直接编写SQL语句的情况下,可以使用knex的数据库事务方法knex.transaction
2.knex事务
事务是关系数据库的一个重要功能,因为它们允许从故障中正确恢复并即使在系统发生故障时也能保持数据库的一致性。事务中的所有查询都在同一数据库连接上执行,并将整组查询作为单个工作单元运行。任何失败都意味着数据库会将在该连接上执行的任何查询回滚到事务前状态。
knex中通过将处理函数传递到 knex.transaction
中进行处理,该处理函数接受一个参数——该参数是一个对象,并可以通过两种方式使用:
- 作为
promise aware
的kenx连接 - 作为一个对象传递到查询进行事务处理,并最终调用提交或回滚。
两种方法的使用示例分别为:
// Using trx as a query builder:
knex.transaction(function(trx) {
const books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
return trx
.insert({name: 'Old Books'}, 'id')
.into('catalogues')
.then(function(ids) {
books.forEach((book) => book.catalogue_id = ids[0]);
return trx('books').insert(books);
});
})
.then(function(inserts) {
console.log(inserts.length + ' new books saved.');
})
.catch(function(error) {
// If we get here, that means that
// neither the 'Old Books' catalogues insert,
// nor any of the books inserts will have taken place.
console.error(error);
});
或通过如下方式使用:
// Using trx as a transaction object:
knex.transaction(function(trx) {
const books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
knex.insert({name: 'Old Books'}, 'id')
.into('catalogues')
.transacting(trx)
.then(function(ids) {
books.forEach((book) => book.catalogue_id = ids[0]);
return knex('books').insert(books).transacting(trx);
})
.then(trx.commit)
.catch(trx.rollback);
})
.then(function(inserts) {
console.log(inserts.length + ' new books saved.');
})
.catch(function(error) {
// If we get here, that means that
// neither the 'Old Books' catalogues insert,
// nor any of the books inserts will have taken place.
console.error(error);
});
上述示例都可以通下列async/await
方法来使用:
try {
await knex.transaction(async trx => {
const books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
const ids = await trx('catalogues')
.insert({
name: 'Old Books'
}, 'id')
books.forEach((book) => book.catalogue_id = ids[0])
const inserts = await trx('books').insert(books)
console.log(inserts.length + ' new books saved.')
})
} catch (error) {
// If we get here, that means that neither the 'Old Books' catalogues insert,
// nor any of the books inserts will have taken place.
console.error(error);
}
或者通过下列方式使用:
// Using trx as a transaction object:
const trx = await knex.transaction();
const books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
trx('catalogues')
.insert({name: 'Old Books'}, 'id')
.then(function(ids) {
books.forEach((book) => book.catalogue_id = ids[0]);
return trx('books').insert(books);
})
.then(trx.commit)
.catch(trx.rollback);
3.knex事务模型
如果您需要为事务指定隔离级别,可以使用配置参数isolationLevel(oracle 和 sqlite 不支持),选项包括:read uncommitted
, read committed
, repeatable read
, snapshot (mssql only)
, serializable
。
使用方法如下:
// Simple read skew example
const isolationLevel = 'read committed';
const trx = await knex.transaction({isolationLevel});
const result1 = await trx(tableName).select();
await knex(tableName).insert({ id: 1, value: 1 });
const result2 = await trx(tableName).select();
await trx.commit();
// result1 may or may not deep equal result2 depending on isolation level
你也可以通过readOnly
参数,将事务模型设置为只读(当前仅支持mysql, postgres, and redshift),使用方法如下:
const trx = await knex.transaction({ readOnly: true });
// 💥 Cannot `INSERT` while inside a `READ ONLY` transaction
const result = await trx(tableName).insert({ id: 1, foo: 'bar' });