Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Cloud development Atomic operations and transactions


May 22, 2021 Mini Program Cloud Development Advanced



Update instructions such as inc, mul, addToSet allow you to perform atomic operations on a record in a cloud database and sub-documents within a record (combined with an anti-paradigm design), but you need to use the transaction capabilities of the cloud database when you want to operate atomically across multiple records or collections.

First, update the atomic operation of the instruction

A relationship database is difficult to represent by the need for data enforcement consistency through a statement and can only rely on transactions. However, cloud development databases typically do not have to use transactions because they can reverse paradigms to design inline sub-documents and updates that specify that atomic operations can be performed on a single record or sub-documents within the same record.

For example, after adjusting the quantity of an order item, the total cost of the order should be updated at the same time, and we can design the collection in the following way, such as the order's collection as an order:

  1. {
  2. "_id": "2020030922100983",
  3. "userID": "124785",
  4. "total":117,
  5. "orders": [{
  6. "item":"苹果",
  7. "price":15,
  8. "number":3
  9. },{
  10. "item":"火龙果",
  11. "price":18,
  12. "number":4
  13. }]
  14. }

Customers often adjust the order of an item such as Apple's purchase quantity, and the total price of the order must be updated synchronously, can not buy the quantity reduced, but the total price is unchanged, these two operations must be carried out at the same time, if the use of the relationship database, you need to first through two queries, update the data, and then stored in the database, this is easy to have some success, some did not succeed. However, a database developed by the cloud can be updated with the help of an update instruction to achieve two data successes or failures at the same time:

  1. db.collection('order').doc('2020030922100983')
  2. .update({
  3. data: {
  4. "orders.0.number": _.inc(1),
  5. "total":_.inc(15)
  6. }
  7. })

This is done only in a single record, so what about the atomic operation to implement across records? Update instructions can actually do transaction simulation, but more troublesome, then it is recommended to use transactions.

Second, transactions and ACID

A transaction is a batch of database statements, but this batch is an atom (atomic), and multiple additions and deletions are bound together, indivisible, executed either or rollback. For example, bank transfer, need to do one account money remitted out, that another account will certainly receive money, can not transfer money out, but the money did not go to another account;

1.ACID

In general, transactions must meet four conditions (ACID): Atomicity (Atomic), Consistency (Stability), Isolation (Isolation), Durability (Reliability):

  • Atomicity: All operations in the entire transaction are either fully committed successfully or rolled back all failed, and it is not possible for a transaction to perform only a portion of them.

  • Consistency: The execution of a transaction does not compromise the integrity and consistency of the database data, and a transaction must be in a consistent state before and after execution. I n other words, the result of a transaction must be a transition from one consistency state to another. For example, before the execution of the transaction, the A user account has 50 yuan, B user account has 150 yuan, after the execution of B transferred to A 50 yuan transaction, the sum of the two user accounts or 200 yuan.

  • Isolation: Isolation of transactions means that in a complex environment, when different transactions manipulate the same data at the same time, each transaction has its own complete data spatial transaction, without interfering with each other. For example, online banking, while the transfer of many people, but will not affect the transfer between A and B;

  • Reliability: Even in the event of a failure such as a system crash or machine downtime, as long as the database can be restarted, it must be able to be restored to the state at the end of a successful transaction, and updates to the committed transaction are not lost.

2. Cloud function transaction considerations

(1) Bulk operations are not supported, only single-record operations are supported

Bulk operations (where statements) are not supported in transactions, only single-record operations (collection.doc, collection.add), which avoids a large number of lock conflicts, guarantees operational efficiency, and in most cases, single-record operations are sufficient because multiple individual records can be operated in a transaction, i.e. both record x and y of collection B in a transaction.

(2) The cloud database uses snapshot isolation

For two transactions that are executed in a synth, problems can occur when it comes to operating the same record. Because of the inconsistencies in the data between the operations, including dirty reads, non-repeatable reads, phantom reads, and so on.

  • Dirty read: refers to when a transaction is accessing the data and the data has been modified, and this modification has not yet been committed to the database, then another transaction also accessed the data, and then used the data;

  • Non-repeatable: The data read twice in one transaction is different and is affected by another transaction's modified commit and is therefore called non-repeatable

  • Phantom read: The first transaction reads the table, and when the second transaction increases or deletes the table after the transaction commits, the first transaction reads again, increasing or decreasing the number of rows

The transactional process of a database system developed by the cloud uses snapshot isolation to avoid data insanity created by the same operation.

  • During a transaction, the read returns a snapshot of the object, not the actual data

  • During a transaction, the write will be: 1. C hange the snapshot to ensure consistency in the next reading; Add a transaction lock to the object

  • Transaction lock: If a transaction lock exists on an object, then: 1. W rites to other transactions will fail directly; Normal update operations are blocked until the transaction lock is released or timed out

  • After the transaction is committed, the completed snapshot is atomically written to the database

Third, the transaction operation of the two sets of APIs

Transactions in the cloud development database provide interfaces in two operating styles, a simple runTransaction interface with conflicting autotry, and a startTransaction interface with process custom control. U sing the parameter transaction obtained in the runTransaction callback or the return value transaction obtained through startTransaction, we liken it to a db object, but the operations performed on it are done in a snapshot within the transaction, ensuring atomicity. A list of interface tree diagrams provided on the transaction:

  1. transaction
  2. |-- collection 获取集合引用
  3. | |-- doc 获取记录引用
  4. | | |-- get 获取记录内容
  5. | | |-- update 更新记录内容
  6. | | |-- set 替换记录内容
  7. | | |-- remove 删除记录
  8. | |-- add 新增记录
  9. |-- rollback 终止事务并回滚
  10. |-- commit 提交事务(仅在使用 startTransaction 时需调用)

1. Get transaction through the runTransaction callback

The following provides a simple example of a transfer between two accounts using the runTransaction interface. T he transaction execution function is passed in by the developer, and the function receives an argument transaction that provides the collection method and the rollback method. The collection method is used to take a database collection record reference for operations, and the rollback method is used to terminate and roll back transactions if they do not want to continue.

  1. const cloud = require('wx-server-sdk')
  2. cloud.init({
  3. env: cloud.DYNAMIC_CURRENT_ENV
  4. })
  5. const _ = db.command
  6. exports.main = async (event) => {
  7. try {
  8. const result = await db.runTransaction(async transaction => {
  9. const aaaRes = await transaction.collection('account').doc('aaa').get()
  10. const bbbRes = await transaction.collection('account').doc('bbb').get()
  11. if (aaaRes.data && bbbRes.data) {
  12. const updateAAARes = await transaction.collection('account').doc('aaa').update({
  13. data: {
  14. amount: _.inc(-10)
  15. }
  16. })
  17. const updateBBBRes = await transaction.collection('account').doc('bbb').update({
  18. data: {
  19. amount: _.inc(10)
  20. }
  21. })
  22. console.log(`transaction succeeded`, result)
  23. return {
  24. aaaAccount: aaaRes.data.amount - 10,
  25. }
  26. } else {
  27. await transaction.rollback(-100)
  28. }
  29. })
  30. return {
  31. success: true,
  32. aaaAccount: result.aaaAccount,
  33. }
  34. } catch (e) {
  35. console.error(`事务报错`, e)
  36. return {
  37. success: false,
  38. error: e
  39. }
  40. }
  41. }

The transaction execution function must be an async asynchronous function or a function that returns Promise, and when the transaction execution function returns, the SDK considers that the user logic is complete and automatically commits the transaction, so it is important to ensure that the user transaction logic is completed before returning or resolveing in the async asynchronous function.

2. Get transaction through startTransaction

  • db.startTransaction(), open a new transaction, and then you can perform CRUD operations;

  • db.startTransaction (.transaction.commit(), commit transaction to save data, changes in the transaction before the submission of the data is not visible to the outside;

  • db.startTransaction (.rollback(), where a transaction terminates and rolls back a transaction, for example, a partial data update fails and the modified data is rolled back.

  1. const cloud = require('wx-server-sdk')
  2. cloud.init({
  3. env: cloud.DYNAMIC_CURRENT_ENV
  4. })
  5. const db = cloud.database({
  6. throwOnNotFound: false,
  7. })
  8. const _ = db.command
  9. exports.main = async (event) => {
  10. try {
  11. const transaction = await db.startTransaction()
  12. const aaaRes = await transaction.collection('account').doc('aaa').get()
  13. const bbbRes = await transaction.collection('account').doc('bbb').get()
  14. if (aaaRes.data && bbbRes.data) {
  15. const updateAAARes = await transaction.collection('account').doc('aaa').update({
  16. data: {
  17. amount: _.inc(-10)
  18. }
  19. })
  20. const updateBBBRes = await transaction.collection('account').doc('bbb').update({
  21. data: {
  22. amount: _.inc(10)
  23. }
  24. })
  25. await transaction.commit()
  26. return {
  27. success: true,
  28. aaaAccount: aaaRes.data.amount - 10,
  29. }
  30. } else {
  31. await transaction.rollback()
  32. return {
  33. success: false,
  34. error: `rollback`,
  35. rollbackCode: -100,
  36. }
  37. }
  38. } catch (e) {
  39. console.error(`事务报错`, e)
  40. }
  41. }

That is, for multi-user simultaneous operations (mainly write) databases to handle the problem of the same time, we can not only use atomic updates, but also use transactions. Where the atomic update primary user manipulates fields within a single record or fields in an array object embedded in a single record, transactions are primarily used for processing across records and collections.