Sequelize is a node.js ORM library that allows you to use familiar JS links to manipulate databases. Work needs, the author’s daily work in addition to cutting diagrams, also began to write back-end code. Due to inexperience, previously developed features failed in concurrent cases. Go back and sort out the questions and take notes.

Basic knowledge of

Transaction: A Transaction is a logical unit of database execution consisting of a limited sequence of database operations. These operations wrapped in transactions have a common execution result, either all successful or all failed and all rolled back. all-or-nothing

Take a bank transfer. User A transfers 100 to user B. Pseudocode (ignore details)

Balance A = balance A - 100 balance B = balance B + 100Copy the code

Account A -100 and account B +100 are two independent statements. If an exception occurs in the middle, resulting in program interruption, account A’s 100 will disappear into thin air. No one wants to see this result, wrapped in transactions to ensure atomicity of the transfer operation. If an error occurs, all are rolled back.

start transaction; // Transfer operationcommit;
Copy the code

Transaction is demonstrated in Terminal.

Sequelize provides a Transaction class that creates transactions from Sequelize.Transaction and sets each database operation to which Transaction the current operation belongs.

await sequelize.transaction({}, async (transaction) => {
    const instance = await Accounts.findOne({
      where: {
        name: 'HelKyle',
      },
      transaction,
    });
    
    await instance.update({
      balances: instance.balances + number,
    }, {
      transaction,
    })
})
Copy the code

As you can see from the Squelize log, the transaction id 444a5AFe-9635-40FD-90d7-10f5AA16077a is created and all subsequent queries and updates are run in this transaction.

Executing (444a5afe-9635-40fd-90d7-10f5aa16077a): START TRANSACTION;
Executing (444a5afe-9635-40fd-90d7-10f5aa16077a): SELECT "name"."balances" FROM "accounts" AS "accounts" WHERE "accounts"."name" = 'HelKyle';
Executing (444a5afe-9635-40fd-90d7-10f5aa16077a): UPDATE "accounts" SET "balances"=The $1 WHERE "name" = $2
Executing (444a5afe-9635-40fd-90d7-10f5aa16077a): COMMIT;
Copy the code

If you don’t want to manually pass a TRANSACTION object every time, you can configure a global default transaction by configuring the CLS.

The tragedy of concurrency

Transactions only solve the problem of atomicity of operations; another thorny problem is concurrency. Suppose that in the process of transfer from A to B, C also happens to transfer 80 to A, and use the form of table to demonstrate the problems that may occur in the concurrent process.

Transaction 1 (Transfer from A to B) Transaction 2 (Transfer from C to A)
  Query balance 200 of A
Query balance 200 of A  
  Update A balance = 200 + 80
Update A balance = 200-100  

As can be seen from the result, A did not receive 80 from C, and user C lost 💰. Concurrency problems can be avoided by locking.

The concept of the lock

Pessimistic locks vs. Optimistic locks:

Pessimistic locking is reserved for the outside world. To avoid conflicts, a record is locked first, and other transactions must wait to perform operations on the record before the current transaction is released.

Transaction 1 (Transfer from A to B) Transaction 2 (Transfer from C to A)
  Query A balance 200 and lock the record
Query the balance of A, find another transaction locked record, wait…  
  Update A balance = 200 + 80, release lock
Get enforcement. Check balance A, 280  
Update A balance = 280-100  

MySql and Postgres both implement pessimistic lock, execute related statement (select for update), no need to develop. The disadvantage of pessimistic locking is that it can affect throughput in scenarios with frequent read operations.

The opposite of pessimism is optimistic locking, which assumes that there are fewer conflicts and that any transaction can read the resource first and make a judgment while writing updates. The version field is usually added and verion + 1 is used for each update. When the update is committed to the database, the version is determined and the version is retried if it has expired.

Transaction 1 (Transfer from A to B) Transaction 2 (Transfer from C to A)
  Query A balance 200 and version N
Query A balance 200 and version N  
  Update A balance = 200 + 80, version number = N + 1
If the latest version is not n, try again  
Query A balance 280, version N + 1  
Update A balance = 280-100, version number = N + 2  

SQL code:

select name, balances, version from accounts where name='HelKyle';

update accounts set version=version+1, balances=balances+100
    where name='HelKyle' and version= # {version}
Copy the code

Optimistic locking can cause retries in scenarios with frequent write operations, which also affects throughput.

Exclusive locks VS shared locks:

Exclusive lock is a pessimistic lock, when the query lock. There can be only one exclusive lock on a resource at a time, and other transactions adding exclusive locks to this record must wait for the current transaction to complete (other transaction reads must wait).

SQL code

select * from accounts where name='HelKyle' for update;
Copy the code

Sequelize writing

await Accounts.findOne({
    where: { name: 'HelKyle' },
    lock: Sequelize.Transaction.LOCK.UPDATE
});
Copy the code

Demo: 👈 transaction does not end when 👉 transaction can only wait until the exclusive lock is released.

A transaction Transaction 2
start transaction; start transaction;
select * from accounts where name=’A’ for update;  
Output: A 100  
  select * from accounts where name=’A’ for update;
  waiting…
commit;  
  Output: A, 100
  commit;

A shared lock allows multiple resources to exist at the same time. You can modify or delete a resource only after all other shared locks are released.

SQL code

select * from accounts where name='HelKyle' for share;
Copy the code

Sequelize writing

await Accounts.findOne({
    where: { name: 'HelKyle' },
    lock: Sequelize.Transaction.LOCK.SHARE
});
Copy the code

Demo: 👈 👉 transaction can be queried, 👈 transaction to modify data, because the 👉 shared lock is not released, the modification operation can only wait.

A transaction Transaction 2
start transaction; start transaction;
select * from accounts where name=’A’ for share;  
Output: A 100  
  select * from accounts where name=’A’ for share;
  Output: A 100
update accounts set balances=10 where name=’A’  
waiting…  
  commit;
set A.balances = 10  
commit;  

Sequelize.transaction (options) configuration parameter isolationLevel: sequelize.Transaction (options)

level Dirty read Unrepeatable read Phantom read
READ_UNCOMMITTED The read is not committed      
READ_COMMITTED Read committed    
REPEATABLE_READ Repeatable read  
SERIALIZABLE SERIALIZABLE

The ❌ of 👆 indicates that at this level, a certain type of problem does not occur.

Noun analysis:

  • Dirty reads are when uncommitted data is read in one transaction from another. If the other transaction ultimately fails and does not write to the database, the first transaction gets the data that does not exist.

    A transaction Transaction 2
    start transaction; start transaction;
    select * from accounts where name=’A’;  
    Output: A 100  
    update accounts set balances=10 where name=’A’  
      select * from accounts where name=’A’;
      Output: A 10 (when the transaction is not committed)
  • Non-repeatable reads describe a transaction that reads a uniform resource multiple times (with no modification in this transaction) and gets different results.

    A transaction Transaction 2
    start transaction; start transaction;
    select * from accounts where name=’A’;  
    Output: A 100  
      update accounts set balances=10 where name=’A’
      commit;
    select * from accounts where name=’A’;  
    Output: A 10  
  • Unreal read, refers to the occurrence of the query conditions, but not before 👀. For example, if the sub becomes 0 over all data in a table in queryAll, the sub becomes 0 because other transactions are writing new content at the same time. Therefore, the sub clearly conforms to queryAll but the sub does not become 0, just like 👻.

    A transaction Transaction 2
    start transaction; start transaction;
    select * from accounts;  
    Output: A 100  
    update accounts set balances=0;  
      insert into accounts values (‘B’, 100);
      commit;
    commit;  
    select * from accounts;  
    Output: A 0, B 100  

Configure isolationLevel in Sequelize

sequelize.transaction({
	isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE
}, transaction => {
  // your transactions
});
Copy the code

A link to the

  • MySQL transaction isolation levels
  • Optimistic and pessimistic locks