Mysql transaction

1. Basic business knowledge

1.1 Overview of database transactions

1.1.1 Storage engine support

SHOW ENGINES command to see which storage engines are currently supported by MySQL, and whether these storage engines support transactions.

It can be seen that in MySQL, only InnoDB supports transactions.

1.1.2 Basic concepts

Transaction: A set of logical units of operation that transforms data from one state to another.

Principles of transaction processing:

It is guaranteed that all transactions are executed as a unit of work, even if there is a failure, this execution method cannot be changed.

When multiple operations are performed in one transaction, either all transactions are submitted (commit), then these modifications are permanently saved; or the database management system will discard all modifications made, and the entire transaction is rolled back (rollback) to initial state.

1.1.3 ACID characteristics of transactions

Atomicity

Atomicity means that a transaction is an indivisible unit of work, either all submitted, or all failed and rolled back.

Consistency

There are errors in the description of consistency on many domestic websites. For details, please refer to Wikipedia's description of Consistency.

By definition, consistency refers to the transformation of data from one legal state to another before and after transaction execution. This state is semantic rather than grammatical, and is related to specific businesses.

So what is the legal data state? A state that satisfies predetermined constraints is called a legal state. In layman's terms, this state is defined by itself (such as satisfying the constraints in the real world). If this state is satisfied, the data is consistent, if this state is not satisfied, the data is inconsistent! If an operation in the transaction fails, the system will automatically cancel the currently executing transaction and return to the state before the transaction operation.

isolation

Transaction isolation means that the execution of a transaction cannot be interfered by other transactions, that is, the operations and data used within a transaction are isolated from other concurrent transactions, and the concurrently executed transactions cannot interfere with each other.

Durability

Persistence means that once a transaction is committed, its changes to the data in the database are permanent, and other subsequent operations and database failures should not have any impact on it.

Persistence is guaranteed through the transaction log. Logs include redo logs and rollback logs. When we modify the data through a transaction, we will first record the change information of the database in the redo log, and then modify the corresponding row in the database. The advantage of this is that even if the database system crashes, after the database restarts, the redo logs that have not been updated in the database system can be found and re-executed, so that the transaction is durable.

1.1.4 Transaction status

We now know that a transaction is an abstract concept, which actually corresponds to one or more database operations. MySQL roughly divides transactions into several states according to the different stages of these operations:

  • active

When the database operation corresponding to the transaction is being executed, we say that the transaction is in an active state.

  • Partially committed

When the last operation in the transaction is executed, but because the operations are executed in memory, the impact is not flushed to disk, we say that the transaction is in a partially committed state.

  • failed

When the transaction is in an active or partially committed state, it may encounter some errors (database itself errors, operating system errors, or direct power failure, etc.) and cannot continue to execute, or artificially stop the execution of the current transaction, we will Says the transaction is in a failed state.

  • aborted

If the transaction has been partially executed and becomes a failed state, then the operations in the modified transaction need to be restored to the state before the transaction was executed. In other words, it is to undo the impact of the failed transaction on the current database. We call this undo process a rollback. When the rollback operation is completed, that is, the database is restored to the state before the execution of the transaction, we say that the transaction is in an aborted state.

  • committed

When a transaction in a partially committed state synchronizes all modified data to disk, we can say that the transaction is in a committed state.

A basic state transition diagram looks like this:

1.2 How to use transactions

There are two ways to use transactions, namely explicit transactions and implicit transactions.

1.2.1 Explicit transactions

Step 1: START TRANSACTION or BEGIN is used to explicitly open a transaction.

mysql> BEGIN; 
#或者 
mysql> START TRANSACTION;

Compared with BEGIN, the START TRANSACTION statement is special in that it can be followed by several modifiers:

  • READ ONLY: Indicates that the current transaction is a read-only transaction, that is, the database operations belonging to the transaction can only read data, but cannot modify data.
  • READ WRITE : Indicates that the current transaction is a read-write transaction, that is, the database operations belonging to the transaction can either read data or modify data.
  • WITH CONSISTENT SNAPSHOT : Start a consistent read.

Step 2: Operations in a series of transactions (mainly DML, excluding DDL)

Step 3: Commit the transaction or abort the transaction (i.e. roll back the transaction)

# 提交事务。当提交事务后,对数据库的修改是永久性的。 
mysql> COMMIT;

# 回滚事务。即撤销正在进行的所有没有提交的修改 
mysql> ROLLBACK; 
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]

1.2.2 Implicit transactions

There is a system variable autocommit in MySQL:

mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)

Of course, if we want to turn off this automatic submission function, we can use one of the following two methods:

  • Explicitly start a transaction using the START TRANSACTION or BEGIN statement. In this way, the automatic commit function will be temporarily turned off before the transaction is committed or rolled back.
  • Set the value of the system variable autocommit to OFF, like this:
SET autocommit = OFF;
#或
SET autocommit = 0;

1.2.3 The case of implicit submission of data

  • Data definition language (Data definition language, abbreviated as: DDL)
  • Implicitly use or modify tables in the mysql database
  • Transaction control or statements about locking
    • When another transaction is started using the START TRANSACTION or BEGIN statement before a transaction has been committed or rolled back, the previous transaction will be committed implicitly. Right now
    • The current value of the autocommit system variable is OFF. When you manually turn it ON, it will also implicitly commit the transaction to which the previous statement belongs.
    • Using LOCK TABLES, UNLOCK TABLES and other statements about locking will also implicitly commit the transaction to which the previous statement belongs.
  • statement to load data
  • Some statements about MySQL replication

1.2.4 Commit and Rollback

Look at the default state of MySQL, what is the final processing result of the following transaction.

Case 1:

CREATE TABLE user(name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
BEGIN;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
SELECT * FROM user;

Running result (1 row of data):

mysql> commit;
Query OK, 0 rows affected (0.00 秒)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 秒)
mysql> INSERT INTO user SELECT '李四';
Query OK, 1 rows affected (0.00 秒)
mysql> INSERT INTO user SELECT '李四';
Duplicate entry '李四' for key 'user.PRIMARY'
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 秒)
mysql> select * from user;
+--------+
| name |
+--------+
| 张三 |
+--------+
1 行于数据集 (0.01 秒)

Case 2:

CREATE TABLE user (name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
mysql> SELECT * FROM user;
+--------+
| name |
+--------+
| 张三 |
| 李四 |
+--------+
2 行于数据集 (0.01 秒)

When autocommit=0 is set, no matter whether START TRANSACTION or BEGIN is used to start the transaction, COMMIT is required to make the transaction take effect, and ROLLBACK is used to roll back the transaction.

When autocommit=1 is set, each SQL statement will be submitted automatically. But at this time, if you use STARTTRANSACTION or BEGIN to explicitly open the transaction, then the transaction will only take effect when COMMIT, and will roll back when ROLLBACK.

1.3 Transaction isolation level

MySQL is a software with a client/server architecture. For the same server, several clients can connect to it. After each client is connected to the server, it can be called a session (Session).

Each client can send a request statement to the server in its own session, and a request statement may be part of a certain transaction, that is, for the server, multiple transactions may be processed at the same time.

Transactions have the characteristics of isolation. In theory, when a transaction accesses a certain data, other transactions should be queued. After the transaction is committed, other transactions can continue to access the data. But this has too much impact on performance. We want to maintain the isolation of transactions, and we want the server to perform as high as possible when processing multiple transactions accessing the same data. It depends on the trade-off between the two.

1.3.1 Data preparation

To create a table:

CREATE TABLE student (
studentno INT,
name VARCHAR(20),
class varchar(20),
PRIMARY KEY (studentno)
) Engine=InnoDB CHARSET=utf8;

Then insert a piece of data into this table:

INSERT INTO student VALUES(1, '小谷', '1班');

1.3.2 Data concurrency issues

For the isolation and concurrency of transactions, how to choose? Let's first look at the problems that may arise when transactions that access the same data are not guaranteed to be executed serially (that is, after one is executed and then the other is executed):

1. 脏写( Dirty Write )

For two transactions Session A and Session B, if transaction Session A modifies the data modified by another uncommitted transaction Session B, it means that dirty writing has occurred

2. Dirty Read

For two transactions Session A and Session B, Session A reads the fields that have been updated by Session B but have not yet been submitted. If Session B rolls back later, the content read by Session A will be temporary and invalid.

Session A and Session B each start a transaction. The transaction in Session B first updates the name column of the record whose studentno is 1 to 'Zhang San', and then the transaction in Session A queries the record with studentno as 1. , if the value of the column name is read as 'Zhang San', and the transaction in Session B is rolled back later, then the transaction in Session A is equivalent to reading a non-existent data, this phenomenon is called Dirty read.

3. Non-repeatable read (Non-Repeatable Read)

For two transactions Session A, Session B, Session A reads a field, and then Session B updates the field. After Session A reads the same field again, the value is different. That means a non-repeatable read has occurred.

We have submitted several implicit transactions in Session B (note that they are implicit transactions, which means that the transaction is submitted when the statement ends), these transactions have modified the value of the column name of the record whose studentno column is 1, and each transaction is submitted Later, if all transactions in Session A can view the latest value, this phenomenon is also called non-repeatable read.

4. Phantom reading (Phantom)

For two transactions Session A, Session B, Session A reads a field from a table, and then Session B inserts some new rows in the table. Later, if Session A reads the same table again, there will be a few more rows. That means a phantom read has occurred.

The transaction in Session A first queries the table student according to the condition studentno > 0, and obtains the record whose name column value is 'Zhang San'; then an implicit transaction is submitted in Session B, and the transaction inserts an entry into the table student A new record; afterward, the transaction in Session A queries the table student according to the same condition studentno > 0, and the result set contains the record newly inserted by the transaction in Session B. This phenomenon is also called phantom reading. We call the newly inserted records phantom records.

1.3.3 Four isolation domains in SQL

The above introduces some problems that may be encountered during the execution of several concurrent transactions. These problems are prioritized. We will rank these problems according to their severity:

Dirty write > Dirty read > Non-repeatable read > Phantom read

Part of the isolation can be sacrificed in exchange for part of the performance here: set up some isolation levels, the lower the isolation level, the more concurrency problems will occur. There are four isolation levels established in the SQL standard:

  • READ UNCOMMITTED

Read uncommitted, at this isolation level, all transactions can see the execution results of other uncommitted transactions. Dirty reads, non-repeatable reads, and phantom reads cannot be avoided.

  • READ COMMITTED

Read committed, which satisfies the simple definition of isolation: a transaction can only see changes made by committed transactions. This is the default isolation level for most database systems (but not for MySQL). Dirty reads can be avoided, but the problems of non-repeatable reads and phantom reads still exist.

  • REPEATABLE READ

Repeatable reading, after transaction A reads a piece of data, transaction B modifies and submits the data at this time, then transaction A reads the data again, and the original content is still read. Dirty reads and non-repeatable reads can be avoided, but phantom reads still exist. This is the default isolation level for MySQL.

  • SERIALIZABLE

Serializability, which ensures that transactions can read the same rows from a table. During the duration of this transaction, other transactions are prohibited from performing insert, update, and delete operations on the table. All concurrency problems can be avoided, but the performance is very poor. Dirty reads, non-repeatable reads, and phantom reads can be avoided.

According to the SQL standard, for different isolation levels, problems of different severity can occur in concurrent transactions. The details are as follows:

Why is dirty writing not involved? Because the problem of dirty writing is too serious, no matter what isolation level it is, dirty writing is not allowed to happen.

Different isolation levels have different phenomena, and have different locks and concurrency mechanisms. The higher the isolation level, the worse the concurrency performance of the database. The relationship between the four transaction isolation levels and concurrency performance is as follows:

1.3.4 Four isolation levels supported by Mysql

The default isolation level of MySQL is REPEATABLE READ, we can manually modify the isolation level of the transaction.

# 查看隔离级别,MySQL 5.7.20的版本之前:
mysql> SHOW VARIABLES LIKE 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

# MySQL 5.7.20版本之后,引入transaction_isolation来替换tx_isolation
# 查看隔离级别,MySQL 5.7.20的版本及之后:
mysql>  SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 1 warning (0.00 sec)

#或者不同MySQL版本中都可以使用的:
SELECT @@transaction_isolation;

1.3.5 How to set the isolation domain of the transaction

Modify the isolation level of the transaction by the following statement:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
SET GLOBAL TRANSACTION_ISOLATION = 隔离级别;
#其中,隔离级别格式:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE

# 举例
mysql> set session TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-COMMITTED          |
+-------------------------+
1 row in set (0.00 sec)

Regarding the impact of using GLOBAL or SESSION when setting:

  • Use the GLOBAL keyword (affects globally):
    • The currently existing session is invalid
    • Only works on sessions generated after the statement is executed
  • Use the SESSION keyword (affects at session scope):
    • Valid for all subsequent transactions of the current session
    • If executed between transactions, it is valid for subsequent transactions
    • This statement can be executed in the middle of an already opened transaction, but it will not affect the currently executing transaction

1.3.6 Examples of different isolation levels

Example: There is an accout table, the balance of user 1 is 100, and the balance of user 2 is 0.

Demo 1. Read Uncommitted Dirty Reads

Set the isolation level to read uncommitted:

The execution flow of transaction 1 and transaction 2 is as follows:

Demo 2: Read Committed

Demo 3: Repeatable Read

Set the isolation level to repeatable read, and the execution flow of the transaction is as follows:

Demo 4: Phantom reading

Demo 5: Sequential

time

transaction 1

transaction 2

T1

set session transaction isolation level serializable ;

start transaction ;

select count(*) from account where id = 4; #The result is 0

T2

set session transaction isolation level serializable ;

start transaction ;

insert into account(id, account) value (4,100); #Cannot submit all the time, need to wait until transaction 1 is submitted

T3

commit;

T4

The insert statement can only be stored in the library.

T5

select count(*) from account where id = 4; #The result is still 0

T6

commit;

T7

select count(*) from account where id = 4; #The result is 1

1.4 Common classification of transactions

From the perspective of transaction theory, transactions can be divided into the following types:

  • Flat Transactions
  • Flat Transactions with Savepoints
  • Chained Transactions
  • Nested Transactions
  • Distributed Transactions

Two, Mysql transaction log

Transactions have four characteristics: atomicity, consistency, isolation, and durability.

So what mechanism are the four characteristics of transactions based on?

  • The isolation of transactions is realized by the locking mechanism.
  • The atomicity, consistency and durability of the transaction are guaranteed by the redo log and undo log of the transaction.
    • REDO LOG is called redo log, which provides rewrite operation and restores the page operation modified by the committed transaction to ensure the durability of the transaction.
    • UNDO LOG is called the rollback log, and the rollback line is recorded to a specific version to ensure the atomicity and consistency of the transaction.

Some DBAs may think that UNDO is the reverse process of REDO, but it is not.

2.1 redo log

2.1.1 Why do we need redo logs

On the one hand, the buffer pool can help us eliminate the gap between the CPU and the disk, and the checkpoint mechanism can ensure the final placement of data. However, because the checkpoint is not triggered every time it is changed, it is processed by the master thread at intervals of. So the worst case is that after the transaction is committed, the buffer pool has just been written, and the database is down, then this piece of data is lost and cannot be recovered.

On the other hand, transactions contain the characteristics of persistence, that is to say, for a committed transaction, even if the system crashes after the transaction is committed, the changes made by this transaction to the database cannot be lost.

So how to ensure this persistence? A simple approach: flush all pages modified by the transaction to disk before the transaction is committed, but there are some problems with this simple and crude approach

Another solution: We just want to make the changes made to the data in the database by the committed transactions permanent. Even if the system crashes later, the changes can be restored after restarting. So we don't actually need to flush all the pages modified by the transaction in memory to disk every time the transaction is committed, we just need to record what has been modified. For example, a transaction changes the value 1 of the byte at offset 100 in page 10 of the system tablespace to 2. We just need to record: update the value at offset 100 of page 10 of tablespace 0 to 2.

2.1.2 Benefits and characteristics of redo logs

  • benefit
    • The redo log reduces the frequency of flushing
    • Redo logs take up very little space
  • features
    • Redo logs are written to disk sequentially
    • During transaction execution, the redo log keeps recording

2.1.3 Composition of redo

Redo log can be simply divided into the following two parts:

  • Redo log buffer (redo log buffer), stored in memory, is volatile.
    • Parameter setting: innodb_log_buffer_size: redo log buffer size, the default is 16M, the maximum value is 4096M, and the minimum value is 1M.
mysql> show variables like '%innodb_log_buffer_size%';
+------------------------+---------+
| Variable_name          | Value   |
+------------------------+---------+
| innodb_log_buffer_size | 1048576 |
+------------------------+---------+
1 row in set, 1 warning (0.17 sec)
  • Redo log files (redo log file), stored in the hard disk, are persistent.

2.1.4 The overall process of redo

Taking an update transaction as an example, the redo log flow process is shown in the following figure:

  1. First read the original data from the disk into the memory, and modify the memory copy of the data
  2. Generate a redo log and write it to the redo log buffer, recording the modified value of the data
  3. When the transaction is committed, the content in the redo log buffer is refreshed to the redo log file, and the redo log file is appended to the write method
  4. Regularly flush the modified data in memory to disk

Write-Ahead Log (pre-log persistence): Before persisting a data page, first persist the corresponding log page in memory.

2.1.5 Redo log flushing strategy

The writing of the redo log is not directly written to the disk. The InnoDB engine will first write the redo log buffer when writing the redo log, and then flush it into the real redo log file at a certain frequency.

What about a certain frequency here? This is what we want to say about the brushing strategy.

Note that the process of flushing the redo log buffer to the redo log file is not really flushed to the disk, but just flushed into the file system cache (page cache) (this is done by modern operating systems to improve file writing efficiency An optimization), the actual writing will be left to the system to decide (for example, the page cache is large enough). Then there is a problem for InnoDB. If it is handed over to the system for synchronization, if the system goes down, the data will also be lost (although the probability of the whole system going down is still relatively small).

In response to this situation, InnoDB gives the innodb_flush_log_at_trx_commit parameter, which controls how to flush the logs in the redo log buffer to the redo log file when the commit commits the transaction. It supports three strategies:

  • Set to 0: It means that the disk operation will not be performed each time the transaction is submitted. (The system defaults master thread to synchronize redo logs every 1s)
  • Set to 1: It means that every time the transaction is committed, it will be synchronized and the disk operation will be performed (default value)
  • Set to 2: It means that only the content of the redo log buffer is written into the page cache every time the transaction is committed, and no synchronization is performed. It is up to the os to decide when to synchronize to disk files.

2.1.6 Demonstration of different brushing strategies

2.1.7 Write redo log buffer process

1. Supplementary concept: Mini-Transaction

A transaction can contain several statements. Each statement is actually composed of several mtrs, and each mtr can contain several redo logs. Draw a picture to show their relationship like this:

2. Redo log is written to log buffer

Each mtr will generate a set of redo logs, and use a schematic diagram to describe the log situation generated by these mtrs:

Different transactions may be executed concurrently, so the mtr between T1 and T2 may be executed alternately.

3. Structure diagram of redo log block

 2.1.8 redo log buffer

1. Related parameter settings

  • innodb_log_group_home_dir : Specifies the path where the redo log file group is located. The default value is ./, which means it is in the data directory of the database. MySQL's default data directory (var/lib/mysql) has two files named ib_logfile0 and ib_logfile1 by default, and the logs in the log buffer are flushed to these two disk files by default. The location of this redo log file can also be modified.
  • innodb_log_files_in_group: Specifies the number of redo log files, named as: ib_logfile0, iblogfile1...iblogfilen. The default is 2, and the maximum is 100.
mysql> show variables like 'innodb_log_files_in_group';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
#ib_logfile0
#ib_logfile1
  • innodb_flush_log_at_trx_commit: The policy that controls redo log flushing to disk, the default is 1.
  • innodb_log_file_size: set the size of a single redo log file, the default value is 48M. The maximum value is 512G. Note that the maximum value refers to the sum of the entire redo log series files, that is, (innodb_log_files_in_group * innodb_log_file_size) cannot be greater than the maximum value of 512G.
mysql> show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+

Modify its size according to the business to accommodate larger transactions. Edit the my.cnf file and restart the database to take effect, as shown below

[root@localhost ~]# vim /etc/my.cnf
innodb_log_file_size=200M

2. Log file group

The total redo log file size is actually: innodb_log_file_size × innodb_log_files_in_group .

If data is written to the redo log file group in a circular manner, will the redo log written later overwrite the redo log written earlier?

certainly! So the designers of InnoDB proposed the concept of checkpoint.

3. checkpoint

If the write pos catches up with the checkpoint, it means that the log file group is full, and no new redo log records can be written at this time. MySQL has to stop, clear some records, and advance the checkpoint.

2.2 Undo log

Redo log is a guarantee of transaction persistence, and undo log is a guarantee of transaction atomicity. The pre-operation of updating data in a transaction is actually to write an undo log first.

2.2.1 How to understand the undo log

Transactions need to guarantee atomicity, that is, the operations in the transaction are either completed or nothing is done. But sometimes there will be some situations in the middle of the execution of the transaction, such as:

  • Situation 1: Various errors may be encountered during transaction execution, such as errors of the server itself, errors of the operating system, or even errors caused by a sudden power failure.
  • Case 2: Programmers can manually enter the ROLLBACK statement during transaction execution to end the execution of the current transaction.

When the above situation occurs, we need to change the data back to the original state. This process is called rollback, which can create a false impression: this transaction does not seem to do anything, so it meets the atomicity requirements.

2.2.2 The role of undo log

  • Function 1: Roll back data
  • Role 2: MVCC

2.2.3 Storage structure of undo

1. Rollback segment and undo page

InnoDB uses a segmented approach to undo log management, which is a rollback segment.

Each rollback segment records 1024 undo log segments, and applies for undo pages in each undo log segment.

  • Before InnoDB1.1 version (excluding 1.1 version), there is only one rollback segment, so the transaction limit supported at the same time is 1024. Although it is sufficient for most applications.
  • Starting from version 1.1, InnoDB supports a maximum of 128 rollback segments, so the limit of concurrent online transactions is increased to 128*1024.
mysql> show variables like '%undo%';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
| innodb_undo_directory    | .\         |
| innodb_undo_log_truncate | OFF        |
| innodb_undo_logs         | 128        |
| innodb_undo_tablespaces  | 0          |
+--------------------------+------------+
5 rows in set, 1 warning (0.00 sec)

2. Rollback segment and transaction

  1. Each transaction will only use one rollback segment, and one rollback segment may serve multiple transactions at the same time.
  2. When a transaction starts, a rollback segment is created. During the transaction, when the data is modified, the original data will be copied to the rollback segment.
  3. In the rollback segment, the transaction will continue to fill the extent until the end of the transaction or all the space is used up. If the current extent is not enough, the transaction will request the expansion of the next extent in the segment. If all the allocated extents are used up, the transaction will overwrite the original extent or extend the new extent if the rollback segment allows it. panel to use.
  4. The rollback segment exists in the undo tablespace. There can be multiple undo tablespaces in the database, but only one undo tablespace can be used at a time.
  5. When a transaction is committed, the InnoDB storage engine does the following two things:
    1. Put the undo log into the list for later purge operation
    2. Determine whether the page where the undo log is located can be reused, if it can be allocated to the next transaction

3. Data classification in rollback segment

  • Uncommitted undo information
  • Committed but not expired rollback data (committed undo information)
  • Transaction has committed and expired data (expired undo information)

2.2.4 Types of undo

In the InnoDB storage engine, the undo log is divided into:

  • insert undo log
  • update undo log

2.2.5 The life cycle of undo log

1. Brief generation process

Only the process of Buffer Pool:

With Redo Log and Undo Log:

2. Detailed generation process

When we do an INSERT:

begin; 
INSERT INTO user (name) VALUES ("tom");

When we do an UPDATE:

UPDATE user SET name='Sun'' WHERE id=1;

UPDATE user SET id=2 WHERE id=1;

3. How undo log is rolled back

Taking the above example as an example, assuming that rollback is executed, the corresponding process should be as follows:

1. Delete the data with id=2 through the undo no=3 log

2. Restore the deletemark of the data with id=1 to 0 through the undo no=2 log

3. Use the undo no=1 log to restore the name of the data with id=1 to Tom

4. Delete the data with id=1 through undo no=0 log

4. Undo log deletion

  • For insert undo log
    • Because the record of the insert operation is only visible to the transaction itself, not to other transactions. Therefore, the undo log can be deleted directly after the transaction is committed without purge operation.
  • For update undo log
    • The undo log may need to provide an MVCC mechanism, so it cannot be deleted when the transaction is committed. Put it into the undo log linked list when submitting, and wait for the purge thread to perform the final deletion.

2.2.6 Summary

The undo log is a logical log. When a transaction is rolled back, it just logically restores the database to its original state.

The redo log is a physical log, which records the physical changes of the data page. The undo log is not the reverse process of the redo log.

Three, lock

Transaction isolation is achieved by the locks described in this chapter.

3.1 Overview

In the database, in addition to the contention of traditional computing resources (such as CPU, RAM, I/O, etc.), data is also a resource shared by many users. In order to ensure data consistency, concurrent operations need to be controlled, so locks are generated. At the same time, the locking mechanism also provides guarantees for the realization of various isolation levels of MySQL. Lock conflict is also an important factor affecting the performance of concurrent access to the database. Therefore, locks are particularly important and more complicated for databases.

3.2 Mysql concurrent transactions access the same records

The cases where concurrent transactions access the same records can be roughly divided into three types.

3.2.1 Read-Read

Read-read situation, that is, concurrent transactions read the same records one after another. The read operation itself does not have any effect on the record and does not cause any problems, so this is allowed to happen.

3.2.2 Write-Write

Write-write situation, that is, concurrent transactions successively make changes to the same record.

In this case, the problem of dirty writing will occur, and any isolation level does not allow this problem to occur. Therefore, when multiple uncommitted transactions make changes to a record one after another, they need to be queued for execution. This queuing process is actually realized through locks. This so-called lock is actually a structure in memory. There is no lock before the transaction is executed. That is to say, there is no lock structure associated with the record at the beginning, as shown in the figure:

When a transaction wants to make changes to this record, it will first check whether there is a lock structure associated with this record in the memory, and if there is not, a lock structure will be generated in memory to associate with it. For example, if transaction T1 wants to make changes to this record, it needs to generate a lock structure associated with it:

Summarize a few statements:

  • unlocked
    • It means that there is no need to generate the corresponding lock structure in the memory, and the operation can be performed directly.
  • The lock is acquired successfully, or the lock is successfully acquired
    • It means that the corresponding lock structure is generated in the memory, and the is_waiting attribute of the lock structure is false, that is, the transaction can continue to perform operations.
  • Failed to acquire the lock, or failed to acquire the lock, or did not acquire the lock
    • It means that the corresponding lock structure is generated in the memory, but the is_waiting attribute of the lock structure is true, that is, the transaction needs to wait, and the operation cannot be continued.

3.2.3 Read-Write

Read-write or write-read, that is, one transaction performs read operations and the other performs modification operations. In this case, dirty reads, non-repeatable reads, and phantom reads may occur.

Each database vendor may have different support for the SQL standard. For example, MySQL has solved the problem of phantom reading at the REPEATABLE READ isolation level.

3.2.4 Solutions to Concurrency Problems

How to solve the problems of dirty reads, non-repeatable reads, and phantom reads? There are actually two possible solutions:

Solution 1: Use multi-version concurrency control (MVCC, explained in the next chapter) for read operations, and lock for write operations.

Ordinary SELECT statements will use MVCC to read records under the READ COMMITTED and REPEATABLE READ isolation levels.

  • Under the READ COMMITTED isolation level, a transaction will generate a ReadView every time it executes a SELECT operation during execution. The existence of ReadView itself ensures that the transaction cannot read the changes made by the uncommitted transaction, that is, it avoids Dirty read phenomenon;
  • Under the REPEATABLE READ isolation level, a ReadView will be generated only when the SELECT operation is executed for the first time during the execution of a transaction, and the ReadView will be reused in subsequent SELECT operations, thus avoiding the problems of non-repeatable reads and phantom reads.

Solution 2: Both read and write operations are locked.

  • Summary and comparison found:
    • If the MVCC method is adopted, the read-write operations do not conflict with each other, and the performance is higher.
    • If the locking method is used, read-write operations need to be queued for each other, which affects performance.

In general, we are willing to use MVCC to solve the problem of concurrent execution of read-write operations, but in some special cases, the business must be executed in a locked manner. The following explains the different types of locks in MySQL.

3.3 Classification of different angles of locks

The classification diagram of locks is as follows:

3.3.1 Types of slave data operations: read lock, write lock

  • Read lock: also known as shared lock, represented by S in English. For the same data, the read operations of multiple transactions can be performed simultaneously without affecting each other and without blocking each other.
  • Write lock: also known as exclusive lock, represented by X in English. Before the current write operation is completed, it will block other write locks and read locks. This ensures that only one transaction can perform writes at a given time and prevents other users from reading the same resource that is being written to.

It should be noted that for the InnoDB engine, read locks and write locks can be added to tables or rows.

3.3.2 From the granularity of data operations: table-level locks, page-level locks, row-level locks

3.3.2.1 Table locks

① Table-level S locks and X locks

When executing SELECT, INSERT, DELETE, and UPDATE statements on a table, the InnoDB storage engine will not add table-level S locks or X locks to the table. When some DDL statements such as ALTER TABLE and DROP TABLE are executed on a certain table, other transactions will block the concurrent execution of statements such as SELECT, INSERT, DELETE, and UPDATE on this table. Similarly, when a SELECT, INSERT, DELETE, and UPDATE statement is executed on a table in a certain transaction, DDL statements executed on this table in other sessions will also be blocked. This process is actually implemented by using a structure called metadata locks (English name: Metadata Locks, MDL for short) at the server layer.

In general, the table-level S locks and X locks provided by the InnoDB storage engine are not used. Only used in some special cases, such as during crash recovery. For example, when the system variable autocommit=0 and innodb_table_locks=1, to manually obtain the S lock or X lock of table t provided by the InnoDB storage engine can be written as follows:

  • LOCK TABLES t READ: The InnoDB storage engine adds a table-level S lock to table t.
  • LOCK TABLES t WRITE: The InnoDB storage engine adds a table-level X lock to table t.

However, try to avoid using manual lock table statements such as LOCK TABLES on tables that use the InnoDB storage engine. They do not provide any additional protection, but only reduce concurrency capabilities. The great thing about InnoDB is that it implements finer-grained row locks. You can learn about the S locks and X locks at the InnoDB table level.

MySQL's table-level lock has two modes: (demonstration of operation with MyISAM table)

  • Table shared read lock (Table Read Lock)
  • Table exclusive write lock (Table Write Lock)

② intention lock (intention lock)

InnoDB supports multiple granularity locking, which allows row-level locks and table-level locks to coexist, and intent locks are one of the table locks.

There are two types of intent locks:

  • Intention shared lock (intention shared lock, IS): The transaction intends to add a shared lock (S lock) to certain rows in the table
    • In order for a transaction to acquire an S lock on some rows, it must first acquire an IS lock on the table. SELECT column FROM table ... LOCK IN SHARE MODE;
  • Intention exclusive lock (intention exclusive lock, IX): The transaction intends to add an exclusive lock (X lock) to certain rows in the table
    • For a transaction to acquire an X lock on some rows, it must first acquire an IX lock on the table. SELECT column FROM table ... FOR UPDATE;

That is: the intent lock is maintained by the storage engine itself, and the user cannot manually operate the intent lock. Before adding a shared/exclusive lock to the data row, InooDB will first obtain the corresponding intent lock of the data table where the data row is located.

Concurrency of intent locks

Intention locks are not mutually exclusive with row-level shared/exclusive locks! Because of this, intent locks do not affect the concurrency when multiple transactions lock exclusive locks on different data rows. (Otherwise we can just use ordinary table locks)

in conclusion:

  1. InnoDB supports multi-granularity locks. In certain scenarios, row-level locks can coexist with table-level locks.
  2. Intention locks are not mutually exclusive, but except that IS is compatible with S, intent locks are mutually exclusive with shared locks/exclusive locks.
  3. IX, IS are table-level locks, and will not conflict with row-level X, S locks. It will only conflict with table-level X and S.
  4. Under the premise of ensuring concurrency, intent locks realize the coexistence of row locks and table locks and meet the requirements of transaction isolation.

③ Self-increasing lock (AUTO-INC lock)

In the process of using MySQL, we can add the AUTO_INCREMENT attribute to a column of the table. Example:

CREATE TABLE `teacher` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Since the id field of this table declares AUTO_INCREMENT, it means that it does not need to be assigned a value when writing an insert statement, and the SQL statement modification is as follows.

INSERT INTO `teacher` (name) VALUES ('zhangsan'), ('lisi');

The above insert statement does not explicitly assign a value to the id column, so the system will automatically assign an incremented value to it.

Now the above insert data we see is just a simple insert mode, and all the ways to insert data are divided into three categories, namely "Simple inserts", "Bulk inserts" and "Mixed-mode inserts".

  • “Simple inserts”
    • A statement that can predetermine the number of rows to be inserted (when the statement is initially processed). Includes single-row and multi-row INSERT...VALUES() and REPLACE statements without nested subqueries. For example, the example we gave above belongs to this type of insertion, and the number of rows to be inserted has been determined.
  • "Bulk inserts"
    • Statements where the number of rows to be inserted (and the number of required auto-increment values) is not known in advance. Such as INSERT ... SELECT , REPLACE ... SELECT and LOAD DATA statements, but not pure INSERT. InnoDB assigns a new value to the AUTO_INCREMENT column each time a row is processed.
  • “Mixed-mode inserts” (Mixed-mode inserts)
    • These are "Simple inserts" statements but specify an auto-increment value for some new rows. For example, INSERT INTO teacher (id,name)VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); only specifies the value of part of the id.
    • Another type of "mixed-mode insert" is INSERT ... ON DUPLICATE KEY UPDATE .

For the above data insertion case, Mysql adopts the self-increasing lock method. The AUTO-INC lock is a special table-level lock that needs to be obtained when inserting data into a table that uses the AUTO-INCREMENT column. When the statement is executed, an AUTO-INC lock is added at the table level, and then an incremental value is assigned to each record to be inserted. After the statement is executed, the AUTO-INC lock is released.

When a transaction is holding the AUTO-INC lock, the insert statements of other transactions must be blocked, which can ensure that the incremental value allocated in a statement is continuous. When we insert a value to the primary key of an AUTO_INCREMENT keyword, each statement must compete for the table lock. Such concurrency potential is very low, so innodb provides different locking mechanisms through different values ​​of innodb_autoinc_lock_mode, To significantly improve the scalability and performance of SQL statements.

innodb_autoinc_lock_mode has three values, corresponding to different locking modes:

1. innodb_autoinc_lock_mode = 0 ("traditional" lock mode)

In this lock mode, all types of insert statements acquire a special table-level AUTO-INC lock for inserts into tables with AUTO_INCREMENT columns. This mode is actually like our example above, that is, whenever an insert is executed, a table-level lock (AUTO-INC lock) will be obtained, so that the auto_increment generated in the statement is in order, and when it is replayed in the binlog, It can be guaranteed that the auto_increment of the data in the master and the slave is the same. Because it is a table-level lock, when the insert is executed in multiple transactions at the same time, the contention for the AUTO-INC lock will limit the concurrency capability.

2. innodb_autoinc_lock_mode = 1 ("continuous" lock mode)

Prior to MySQL 8.0, sequential locking mode was the default. In this mode, "bulk inserts" still use the AUTO-INC table-level lock and hold it until the end of the statement. This applies to all INSERT ... SELECT , REPLACE ... SELECT and LOAD DATA statements. Only one statement can hold the AUTO-INC lock at a time. For "Simple inserts" (the number of rows to be inserted is known in advance), table-level AUTO-INC locks are avoided by obtaining the required number of auto-increment values ​​under the control of a mutex (lightweight lock), which is only used during allocation for the duration of the statement, rather than until the statement completes. Table-level AUTO-INC locks are not used unless the AUTO-INC lock is held by another transaction. If another transaction holds the AUTO-INC lock, "Simpleinserts" waits for the AUTO-INC lock as if it were a "bulk inserts".

3. innodb_autoinc_lock_mode = 2 ("interleaved" lock mode)

Beginning with MySQL 8.0, interleaved lock mode is the default. In this lock mode, the auto-increment value is guaranteed to be unique and monotonically increasing across all concurrently executing insert statements of all types. However, because multiple statements can generate numbers at the same time (that is, cross-numbering across statements), the values ​​generated for the rows inserted by any given statement may not be consecutive.

④ Metadata lock (MDL lock)

MySQL 5.5 introduced meta data locks, referred to as MDL locks, which belong to the category of table locks. The role of MDL is to ensure the correctness of reading and writing. For example, if a query is traversing the data in a table, and another thread changes the table structure during execution and adds a column, then the result obtained by the query thread does not match the table structure, and it must not work.

Therefore, when adding, deleting, modifying and querying a table, add an MDL read lock;

When doing structure change operations on the table, add MDL write lock.

3.3.2.2 Row lock

① Record Locks

A record lock is to lock only one record. The official type name is: LOCK_REC_NOT_GAP . For example, the schematic diagram of adding a record lock to the record with the id value of 8 is shown in the figure. It only locks the record with the id value of 8, and has no effect on the surrounding data.

Example:

Session1

Session2

mysql> set autocommit =0 ;

mysql> set autocommit =0 ;

mysql> update student set name = '张三' where id = 1;

mysql> update student set name = '李四1' where id = 3;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> update student set name = '张三1' where id = 1;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

mysql> update student set name = '张三1' where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

Record locks are divided into S locks and X locks, which are called S-type record locks and X-type record locks.

  • When a transaction acquires the S-type record lock of a record, other transactions can continue to acquire the S-type record lock of the record, but cannot continue to acquire the X-type record lock;
  • When a transaction acquires the X-type record lock of a record, other transactions can neither continue to acquire the S-type record lock nor the X-type record lock of the record.

② Gap Locks

MySQL can solve the problem of phantom reading under the REPEATABLE READ isolation level. There are two solutions, which can be solved by using the MVCC solution or by using the locking solution.

But there is a big problem when using the locking solution, that is, when the transaction performs the read operation for the first time, those phantom records do not exist yet, and we cannot add record locks to these phantom records.

InnoDB proposes a lock called Gap Locks, the official type name is: LOCK_GAP, we can simply call it a gap lock. For example, the schematic diagram of adding a gap lock to the record with the id value of 8 is as follows.

图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录 ,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。

gap锁的提出仅仅是为了防止插入幻影记录而提出的。

③ 临键锁(Next-Key Locks)

有时候我们既想 锁住某条记录 ,又想阻止其他事务在该记录前边的间隙插入新记录 ,所以InnoDB就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为next-key锁 。Next-Key Locks是在存储引擎 innodb 、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

begin; 
select * from student where id <=8 and id > 3 for update;

④ 插入意向锁(Insert Intention Locks)

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁 ( next-key锁也包含 gap锁 ),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙 中 插入 新记录,但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks ,官方的类型名称为:LOCK_INSERT_INTENTION ,我们称为插入意向锁 。

插入意向锁是一种 Gap锁 ,不是意向锁,在insert操作时产生。插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁 。

3.3.2.3 页锁

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

3.3.3 从对待锁的态度上:乐观锁、悲观锁

从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想 。

3.3.3.1 悲观锁

悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

3.3.3.2 乐观锁

乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者CAS机制实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁。

1. 乐观锁的版本号机制

在表中设计一个 版本字段 version ,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行 UPDATE ... SET version=version+1 WHERE version=version 。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

2. 乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。

能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

3.3.3.2 两种锁的适用场景

从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:

  1. 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现 , 不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  2. 悲观锁适合写操作多的场景,因为写的操作具有排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。

3.3.4 按加锁的方式划分:显示锁、隐式锁

3.3.4.1 隐式锁

  • 情景一:对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的 事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )。
  • 情景二:对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的 PageHeader 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一的做法。

session 1:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert INTO student VALUES(34,"周八","二班");
Query OK, 1 row affected (0.00 sec)

session 2:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student lock in share mode; #执行完,当前事务被阻塞

执行下述语句,输出结果:

mysql> SELECT * FROM performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 4571466880:4212:4:9:4999713136
REQUESTING_ENGINE_TRANSACTION_ID: 281479548177536
            REQUESTING_THREAD_ID: 48
             REQUESTING_EVENT_ID: 80
REQUESTING_OBJECT_INSTANCE_BEGIN: 4999713136
         BLOCKING_ENGINE_LOCK_ID: 4571467672:4212:4:9:4999717400
  BLOCKING_ENGINE_TRANSACTION_ID: 99918
              BLOCKING_THREAD_ID: 48
               BLOCKING_EVENT_ID: 80
  BLOCKING_OBJECT_INSTANCE_BEGIN: 4999717400
1 row in set (0.00 sec)

隐式锁的逻辑过程如下:

A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。

B. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将隐式锁转换为显式锁 (就是为该事务添加一个锁)。

C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E。

D. 等待加锁成功,被唤醒,或者超时。

E. 写数据,并将自己的trx_id写入trx_id字段。

3.3.4.2 显式锁

通过特定的语句进行加锁,我们一般称之为显示加锁,例如:

显示加共享锁:

select .... lock in share mode

显示加排它锁:

select .... for update

3.3.5 其他锁:全局锁

全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份 。

全局锁的命令:

Flush tables with read lock

3.3.6 其他锁:死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死锁示例:

事务1

事务2

start transaction;

update student set money=10 where id=1;

start transaction;

update account set money=10 where id=2;

update account set money=20 where id=2;

update account set money=20 where id=1;

这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有 两种策略 :

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on ,表示开启这个逻辑

第二种策略的成本分析

方法1:如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是 业务无损 的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的

方法2:控制并发度。如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。这个并发控制要做在 数据库服务端 。如果你有中间件,可以考虑在 中间件实现 ;甚至有能力修改MySQL源码的人,也可以做在MySQL里面。基本思路就是,对于相同行的更新,在进入引擎之前排队,这样在InnoDB内部就不会有大量的死锁检测工作了。

3.4 锁的内存结构 

InnoDB 存储引擎中的 锁结构 如下:

结构解析:

1. 锁所在的事务信息 :

不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个 锁结构 ,这里就记录这个事务的信息。

此 锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比方说事务id等。

2. 索引信息 :

对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。

3. 表锁/行锁信息 :

表锁结构 和 行锁结构 在这个位置的内容是不同的:

  • 表锁:
    • 记载着是对哪个表加的锁,还有其他的一些信息。
  • 行锁:记载了三个重要的信息:
    • Space ID :记录所在表空间。
    • Page Number :记录所在页号。
    • n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits 属性代表使用了多少比特位。
      • n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构

4. type_mode

这是一个32位的数,被分成了 lock_mode 、 lock_type 和 rec_lock_type 三个部分,如图所示

  • 锁的模式( lock_mode ),占用低4位,可选的值如下:
    • LOCK_IS (十进制的 0 ):表示共享意向锁,也就是 IS锁 。
    • LOCK_IX (十进制的 1 ):表示独占意向锁,也就是 IX锁 。
    • LOCK_S (十进制的 2 ):表示共享锁,也就是 S锁 。
    • LOCK_X (十进制的 3 ):表示独占锁,也就是 X锁 。
    • LOCK_AUTO_INC (十进制的 4 ):表示 AUTO-INC锁 。

在InnoDB存储引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表级锁的模式,LOCK_S和LOCK_X既可以算是表级锁的模式,也可以是行级锁的模式。

  • 锁的类型( lock_type ),占用第5~8位,不过现阶段只有第5位和第6位被使用:
    • LOCK_TABLE ,当第5个比特位置为1时,表示表级锁。
    • LOCK_REC ),第6个比特位置为1时,表示行级锁。
  • 行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在 lock_type 的值为LOCK_REC 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
    • LOCK_ORDINARY:表示 next-key锁 。
    • LOCK_GAP:当第10个比特位置为1时,表示 gap锁 。
    • LOCK_REC_NOT_GAP:当第11个比特位置为1时,表示正经 记录锁 。
    • LOCK_INSERT_INTENTION :当第12个比特位置为1时,表示插入意向锁。
    • 其他的类型:还有一些不常用的类型我们就不多说了。
  • is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode 这个32位的数字中:
    • LOCK_WAIT (十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为false ,也就是当前事务获取锁成功。

5. 其他信息

为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。

6. 一堆比特位

如果是行锁结构的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits属性表示的。InnoDB数据页中的每条记录在记录头信息中都包含一个 heap_no 属性,伪记录Infimum 的heap_no值为0 , Supremum 的heap_no值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no ,即一个比特位映射到页内的一条记录。

3.5 锁监控

3.5.1 InnoDB_row_lock

关于MySQL锁的监控,我们一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况。

mysql> show status like 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0      |
| Innodb_row_lock_time          | 145647 |
| Innodb_row_lock_time_avg      | 29129  |
| Innodb_row_lock_time_max      | 50188  |
| Innodb_row_lock_waits         | 5      |
+-------------------------------+--------+
5 rows in set (0.00 sec)

对各个状态量的说明如下:

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
  • Innodb_row_lock_time :从系统启动到现在锁定总时间长度;(等待总时长)
  • Innodb_row_lock_time_avg :每次等待所花平均时间;(等待平均时长)
  • Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
  • Innodb_row_lock_waits :系统启动后到现在总共等待的次数;(等待总次数)

对于这5个状态变量,比较重要的3个见上面(橙色)。

3.5.2 其他监控方法

MySQL把事务和锁的信息记录在了 information_schema 库中,涉及到的三张表分别是INNODB_TRX 、 INNODB_LOCKS 和 INNODB_LOCK_WAITS 。

MySQL5.7及之前 ,可以通过information_schema.INNODB_LOCKS查看事务的锁情况,但只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。

MySQL8.0删除了information_schema.INNODB_LOCKS,添加了 performance_schema.data_locks ,可以通过performance_schema.data_locks查看事务的锁情况,和MySQL5.7及之前不同,performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁。

同时,information_schema.INNODB_LOCK_WAITS也被 performance_schema.data_lock_waits 所代替。

我们模拟一个锁等待的场景,以下是从这三张表收集的信息锁等待场景,我们依然使用记录锁中的案例,当事务2进行等待时,查询情况如下:

Session1

Session2

mysql> begin;

mysql> begin;

mysql> update student set name = '张三' where id = 1;

mysql> update student set name = '李四1' where id = 3;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> update student set name = '张三1' where id = 1;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

mysql> update student set name = '张三1' where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

1、查询正在被锁阻塞的sql语句

SELECT * FROM information_schema.INNODB_TRX\G;

2、查询锁等待情况

mysql> use performance_schema;
Database changed
mysql> SELECT * FROM data_lock_waits\G;
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 4571467672:4212:4:8:4999718088
REQUESTING_ENGINE_TRANSACTION_ID: 99927
            REQUESTING_THREAD_ID: 49
             REQUESTING_EVENT_ID: 78
REQUESTING_OBJECT_INSTANCE_BEGIN: 4999718088
         BLOCKING_ENGINE_LOCK_ID: 4571466880:4212:4:8:4999712792
  BLOCKING_ENGINE_TRANSACTION_ID: 99926
              BLOCKING_THREAD_ID: 48
               BLOCKING_EVENT_ID: 95
  BLOCKING_OBJECT_INSTANCE_BEGIN: 4999712792
1 row in set (0.00 sec)

3、查询锁的情况

mysql> SELECT * from performance_schema.data_locks\G;
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571467672:5273:4983917304
ENGINE_TRANSACTION_ID: 99927
            THREAD_ID: 49
             EVENT_ID: 76
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 4983917304
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571467672:4212:4:15:4999717400
ENGINE_TRANSACTION_ID: 99927
            THREAD_ID: 49
             EVENT_ID: 76
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 4999717400
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 3
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571466880:5273:4983916280
ENGINE_TRANSACTION_ID: 99926
            THREAD_ID: 48
             EVENT_ID: 95
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 4983916280
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571466880:4212:4:8:4999712792
ENGINE_TRANSACTION_ID: 99926
            THREAD_ID: 48
             EVENT_ID: 95
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 4999712792
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 1
4 rows in set (0.00 sec)

从锁的情况可以看出来,两个事务分别获取了IX锁,我们从意向锁章节可以知道,IX锁互相时兼容的。所以这里不会等待,但是事务1同样持有X锁,此时事务2也要去同一行记录获取X锁,他们之间不兼容,导致等待的情况发生。

3.6 附录

间隙锁加锁规则(共11个案例)

间隙锁是在可重复读隔离级别下才会生效的: next-key lock 实际上是由间隙锁加行锁实现的,如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分。而在读提交隔离级别下间隙锁就没有了,为了解决可能出现的数据和日志不一致问题,需要把binlog 格式设置为 row 。也就是说,许多公司的配置为:读提交隔离级别加 binlog_format=row。业务不需要可重复读的保证,这样考虑到读提交下操作数据的锁范围更小(没有间隙锁),这个选择是合理的。

next-key lock的加锁规则

总结的加锁规则里面,包含了两个 “ “ 原则 ” ” 、两个 “ “ 优化 ” ” 和一个 “bug” 。

  • 原则 1 :加锁的基本单位是 next-key lock 。 next-key lock 是前开后闭区间。
  • 原则 2 :查找过程中访问到的对象才会加锁。任何辅助索引上的锁,或者非索引列上的锁,最终都要回溯到主键上,在主键上也要加一把锁。
  • 优化 1 :索引上的等值查询,给唯一索引加锁的时候, next-key lock 退化为行锁。也就是说如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁
  • 优化 2 :索引上(不一定是唯一索引)的等值查询,向右遍历时且最后一个值不满足等值条件的时候, next-keylock 退化为间隙锁。
  • 一个 bug :唯一索引上的范围查询会访问到不满足条件的第一个值为止。

我们以表test作为例子,建表语句和初始化语句如下:其中id为主键索引

CREATE TABLE `test` (
`id` int(11) NOT NULL,
`col1` int(11) DEFAULT NULL,
`col2` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`col1`)
) ENGINE=InnoDB;
insert into test values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

案例一:唯一索引等值查询间隙锁

sessionA

sessionB

sessionC

update test set col2 = col2+1 where id=7;

insert into test values(8,8,8);

(blocked)

update test set col2 = col2+1 where id=10;

(Query OK)

由于表 test 中没有 id=7 的记录.

根据原则 1 ,加锁单位是 next-key lock , session A 加锁范围就是 (5,10] ; 同时根据优化 2 ,这是一个等值查询 (id=7) ,而 id=10 不满足查询条件, next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)

案例二:非唯一索引等值查询锁

sessionA

sessionB

sessionC

select id from test where col1 = 5 lock in share mode;

update test set col2 = col2+1 where id=5;(Query OK)

insert into test values(7,7,7)

(blocked)

这里 session A 要给索引 col1 上 col1=5 的这一行加上读锁。

  1. 根据原则 1 ,加锁单位是 next-key lock ,左开右闭,5是闭上的,因此会给 (0,5] 加上 next-key lock。
  2. 要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的(可能有col1=5的其他记录),需要向右遍历,查到c=10 才放弃。根据原则 2 ,访问到的都要加锁,因此要给 (5,10] 加next-key lock 。
  3. 但是同时这个符合优化 2 :等值判断,向右遍历,最后一个值不满足 col1=5 这个等值条件,因此退化成间隙锁 (5,10) 。
  4. 根据原则 2 , 只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。

但 session C 要插入一个 (7,7,7) 的记录,就会被 session A 的间隙锁 (5,10) 锁住 这个例子说明,锁是加在索引上的。执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。如果你要用 lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,因为覆盖索引不会访问主键索引,不会给主键索引上加锁.

案例三:主键索引范围查询锁

上面两个例子是等值查询的,这个例子是关于范围查询的,也就是说下面的语句

select * from test where id=10 for updates elect * from tets where id>=10 and id<11 for update;

这两条查语句肯定是等价的,但是它们的加锁规则不太一样

sessionA

sessionB

sessionC

select * from test where id>= 10 and id

insert into testvalues(8,8,8);

(Query OK)

insert into testvalues(13,13,13);(blocked)

update test set clo2=col2+1where id=15;(blocked)

  1. 开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10] 。 根据优化 1 ,主键id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
  2. 它是范围查询, 范围查找就往后继续找,找到 id=15 这一行停下来,不满足条件,因此需要加next-key lock(10,15] 。

session A 这时候锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15] 。首次 session A 定位查找id=10 的行的时候,是当做等值查询来判断的,而向右扫描到 id=15 的时候,用的是范围查询判断。

案例四:非唯一索引范围查询锁

select * from test where col1=10 for updates elect * from tets where col1>=10 and col1<11 for update;

与案例三不同的是,案例四中查询语句的 where 部分用的是字段 c ,它是普通索引这两条查语句肯定是等价的,但是它们的加锁规则不太一样

sessionA

sessionB

sessionC

select * from test where col1>= 10 and col1

insert into testvalues(8,8,8);

((blocked)

update test set clo2=col2+1 where id=15;(blocked)

在第一次用 col1=10 定位记录的时候,索引 c 上加了 (5,10] 这个 next-key lock 后,由于索引 col1 是非唯一索引,没有优化规则,也就是 说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和(10,15] 这两个 next-keylock 。

这里需要扫描到 col1=15 才停止扫描,是合理的,因为 InnoDB 要扫到 col1=15 ,才知道不需要继续往后找了。

案例五:唯一索引范围查询锁 bug

sessionA

sessionB

sessionC

select * from test where id> 10 andid

update test set clo2=col2+1where id=20;(blocked)

insert into testvalues(16,16,16);(blocked)

session A 是一个范围查询,按照原则 1 的话,应该是索引 id 上只加 (10,15] 这个 next-key lock ,并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了。

但是实现上, InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20 。而且由于这是个范围扫描,因此索引 id 上的 (15,20] 这个 next-key lock 也会被锁上。照理说,这里锁住 id=20 这一行的行为,其实是没有必要的。因为扫描到 id=15 ,就可以确定不用往后再找了。

案例六:非唯一索引上存在 " " 等值 " " 的例子

这里,我给表 t 插入一条新记录:insert into t values(30,10,30);也就是说,现在表里面有两个c=10的行但是它们的主键值 id 是不同的(分别是 10 和 30 ),因此这两个c=10 的记录之间,也是有间隙的。

sessionA

sessionB

sessionC

delete from test where col1=10;

insert into test values(12,12,12);(blocked)

update test set col2=col2+1 where col1=15;(blocked)

这次我们用 delete 语句来验证。注意, delete 语句加锁的逻辑,其实跟 select ... for update 是类似的,也就是我在文章开始总结的两个 “ 原则 ” 、两个 “ 优化 ” 和一个 “bug” 。

这时, session A 在遍历的时候,先访问第一个 col1=10 的记录。同样地,根据原则 1 ,这里加的是(col1=5,id=5) 到 (col1=10,id=10) 这个 next-key lock 。

由于c是普通索引,所以继续向右查找,直到碰到 (col1=15,id=15) 这一行循环才结束。根据优化 2 ,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成 (col1=10,id=10) 到 (col1=15,id=15) 的间隙锁.

 这个 delete 语句在索引 c 上的加锁范围,就是上面图中蓝色区域覆盖的部分。这个蓝色区域左右两边都是虚线,表示开区间,即 (col1=5,id=5) 和 (col1=15,id=15) 这两行上都没有锁.

案例七: limit 语句加锁

例子 6 也有一个对照案例,场景如下所示:

sessionA

sessionB

delete from test where col1=10 limit 2;

insert into test values(12,12,12);(Query OK)

session A 的 delete 语句加了 limit 2 。你知道表 t 里 c=10 的记录其实只有两条,因此加不加 limit 2 ,删除的效果都是一样的。但是加锁效果却不一样

这是因为,案例七里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (col1=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。因此,索引 col1 上的加锁范围就变成了从( col1=5,id=5)到( col1=10,id=30) 这个前开后闭区间,如下图所示:

 这个例子对我们实践的指导意义就是, 在删除数据的时候尽量加 limit 。

这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

案例八:一个死锁的例子

sessionA

sessionB

select id from test where col1=10 lockin share mode;

update test set col2=col2+1 where c=10;

(blocked)

insert into test values(8,8,8);

ERROR 1213(40001):Deadlock found when trying togetlock;try restarting transaction

  1. session A 启动事务后执行查询语句加 lock in share mode ,在索引 col1 上加了 next-keylock(5,10] 和间隙锁 (10,15) (索引向右遍历退化为间隙锁);
  2. session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待; 实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 col1=10 的行锁,因为sessionA上已经给这行加上了读锁,此时申请死锁时会被阻塞
  3. 然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁, InnoDB 让session B 回滚

案例九:order by索引排序的间隙锁1

如下面一条语句

select * from test where id>9 and id<12 order by id desc for update;

下图为这个表的索引id的示意图。

  1. 首先这个查询语句的语义是 order by id desc ,要拿到满足条件的所有行,优化器必须先找到 “ 第一个 id
  2. 这个过程是通过索引树的搜索过程得到的,在引擎内部,其实是要找到 id=12 的这个值,只是最终没找到,但找到了 (10,15) 这个间隙。( id=15 不满足条件,所以 next-key lock 退化为了间隙锁 (10,15) 。)
  3. 然后向左遍历,在遍历过程中,就不是等值查询了,会扫描到 id=5 这一行,又因为区间是左开右闭的,所以会加一个next-key lock (0,5] 。 也就是说,在执行过程中,通过树搜索的方式定位记录的时候,用的是 “ 等值查询 ” 的方法。

案例十:order by索引排序的间隙锁2

sessionA

sessionB

select * from test where col1>=15 and c

insert into testvalues(6,6,6);

(blocked)

  1. 由于是 order by col1 desc ,第一个要定位的是索引 col1 上 “ 最右边的 ”col1=20 的行。这是一个非唯一索引的等值查询:左开右闭区间,首先加上 next-key lock (15,20] 。 向右遍历,col1=25不满足条件,退化为间隙锁 所以会加上间隙锁(20,25) 和 next-key lock (15,20] 。
  2. 在索引 col1 上向左遍历,要扫描到 col1=10 才停下来。同时又因为左开右闭区间,所以 next-keylock 会加到 (5,10] ,这正是阻塞session B 的 insert 语句的原因。
  3. 在扫描过程中, col1=20 、 col1=15 、 col1=10 这三行都存在值,由于是 select * ,所以会在主键id 上加三个行锁。 因此, session A 的 select 语句锁的范围就是:1. 索引 col1 上 (5, 25) ;2. 主键索引上 id=15 、 20 两个行锁。

案例十一:update修改数据的例子-先插入后删除

sessionA

sessionB

select col1 from test where col1>5 lock in share mode;

update test set col1=1 where col1=5

(Query OK)

update test set col1=5 where col1=1;

(blocked)

注意:根据 col1>5 查到的第一个记录是 col1=10 ,因此不会加 (0,5] 这个 next-key lock 。

session A 的加锁范围是索引 col1 上的 (5,10] 、 (10,15] 、 (15,20] 、 (20,25] 和(25,supremum] 。

之后 session B 的第一个 update 语句,要把 col1=5 改成 col1=1 ,可以理解为两步:

  1. 插入 (col1=1, id=5) 这个记录;
  2. 删除 (col1=5, id=5) 这个记录。

通过这个操作, session A 的加锁范围变成了图示的样子:

 接下来 session B 要执行 update t set col1 = 5 where col1 = 1 这个语句了,一样地可以拆成两步:

  1. 插入 (col1=5, id=5) 这个记录;
  2. 删除 (col1=1, id=5) 这个记录。 第一步试图在已经加了间隙锁的 (1,10) 中插入数据,所以就被堵住了。

四、多版本并发控制

4.1 什么是MVCC

MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制 。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

MVCC没有正式的标准,在不同DBMS中MVCC的实现方式可能是不同的,这里讲的是InnoDB的MVCC实现机制(Mysql的其他存储引擎不支持MVCC)。

4.2 快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突 ,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读 , 而非当前读 。

当前读实际上是一种加锁的操作,是悲观锁的实现。

而MVCC本质是采用乐观锁思想的一种方式。

4.2.1 快照读

快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;比如这样:

SELECT * FROM player WHERE ...

之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。

既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

4.2.2 当前读

当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:

SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁

4.3 复习

4.3.1 再谈隔离级别

我们知道事务有 4 个隔离级别,可能存在三种并发问题:

 另图:

 4.3.2 隐藏字段、Undo log 版本链

回顾一下undo日志的版本链,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。

  • trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id 隐藏列。
  • roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

假设插入记录的事务id是8.那么该条记录示意图如下:

 insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的UndoLog Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。

假设之后两个事务id分别为 10 、 20 的事务对这条记录进行 UPDATE 操作,操作流程如下:

顺序

事务10

事务20

1

BEGIN;

2

BEGIN;

3

UPDATE student SET name="李四"WHERE id=1;

4

UPDATE student SET name="王五"WHERE id=1;

5

COMMIT;

6

UPDATE student SET name="钱七"WHERE id=1;

7

UPDATE student SET name="宋八"WHERE id=1;

8

COMMIT;

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性( INSERT 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:

 对该记录每次更新后,都会将旧值放到一条undo志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链 ,版本链的头节点就是当前记录最新的值。

每个版本中还包含生成该版本时对应的事务id 。

4.4 ReadView

MVCC 的实现依赖于:隐藏字段、Undo Log、Read View。

4.4.1 概念

在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo log中。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时候就需要用到ReadView了,它帮我们解决了行的可见性问题。

ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会产生数据库系统当前的一个快照,InnoDB为每一个事务创建了一个数组,用来记录并维护当前活跃事务的ID(活跃指的是,启动了但还没提交)。

4.4.2 设计思路

使用READ UNCOMMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

使用SERIALIZABLE隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。

使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。

这个ReadView中主要包含4个比较重要的内容,分别如下:

  1. creator_trx_id ,创建这个 Read View 的事务 ID。
  2. trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。
  3. up_limit_id ,活跃的事务中最小的事务 ID。
  4. low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。

注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。

4.4.3 ReadView规则

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。

  • 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的 up_limit_id 和 low_limit_id 之间,那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。
    • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
    • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

4.4.4 MVCC整体操作流程

了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:

  1. 首先获取事务自己的版本号,也就是事务 ID;
  2. 获取 ReadView;
  3. 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
  4. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
  5. 最后返回符合规则的数据。

在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View。如表所示:

 注意,此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。

当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示:

4.5 举例说明

4.5.1 READ COMMITTED隔离级别

READ COMMITTED :每次读取数据前都生成一个ReadView。

现在有两个 事务id 分别为 10 、 20 的事务在执行:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...

此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:

 假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'

之后,我们把 事务id 为 10 的事务提交一下:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;

然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:

# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student中 id 为 1 的记录的版本链就长这样:

 然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'王五'

4.5.2 REPEATABLE READ隔离级别

使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了。

比如,系统里有两个事务id分别为 10 、 20 的事务在执行:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...

此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:

 假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'

之后,我们把 事务id为10的事务提交一下:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;

然后再到事务id为20的事务中更新一下表student中id为1的记录:

# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student中 id 为 1 的记录的版本链就长这样:

 然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍为'张三'

4.5.3 如何解决幻读

接下来说明InnoDB是如何解决幻读的。

假设现在表 student 中只有一条数据,数据内容中,主键 id=1,隐藏的 trx_id=10,它的undo log 如下图所示。

 假设现在有事务A和事务B并发执行, 事务A的事务id为20,事务B的事务id为30。

步骤1:事务A开始第一次查询数据,查询的SQL语句如下。

select * from student where id >= 1;

在开始查询之前,MySQL会为事务A产生一个 ReadView,此时 ReadView的内容如下:

trx_ids=[20,30] , up_limit_id=20 , low_limit_id=31 , creator_trx_id=20 。

由于此时表student中只有一条数据,且符合where id>=1条件,因此会查询出来。然后根据 ReadView机制,发现该行数据的trx_id=10,小于事务A的ReadView里up_limit_id,这表示这条数据是事务A开启之前,其他事务就已经提交了的数据,因此事务A可以读取到。

结论:事务A的第一次查询,能读取到一条数据,id=1。

步骤2:接着事务 B(trx_id=30),往表 student 中新插入两条数据,并提交事务。

insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');

此时表student中就有三条数据了,对应的undo 如下图所示:

 步骤3:接着事务A开启第二次查询,根据可重复读隔离级别的规则,此时事务A并不会再重新生成ReadView。此时表student中的3 条数据都满足where id>=1的条件,因此会先查出来。然后根据ReadView机制,判断每条数据是不是都可以被事务A看到。

1)首先id=1的这条数据,前面已经说过了,可以被事务A看到。

2)然后是id=2的数据,它的trx_id=30,此时事务A发现,这个值处于up_limit_id 和low_limit_id 之间,因此还需要再判断30是否处于trx_ids 数组内。由于事务A的trx_ids=[20,30],因此在数组内,这表示id=2的这条数据是与事务A在同一时刻启动的其他事务提交的,所以这条数据不能让事务A看到。

3)同理,id=3 的这条数据,trx_id也为30,因此也不能被事务A看见。

结论:最终事务A的第二次查询,只能查询出id=1的这条数据。这和事务A的第一次查询的结果是一样的,因此没有出现幻读现象,所以说在MySQL的可重复读隔离级别下,不存在幻读问题。

4.6 总结

这里介绍了MVCC在 READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的读-写 、 写-读 操作并发执行,从而提升系统性能。

核心点在于ReadView的原理, READ COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是生成ReadView的时机不同:

  • READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

五、其他数据库日志

千万不要小看日志。很多看似奇怪的问题,答案往往就藏在日志里。很多情况下,只有通过查看日志才能发现问题的原因,真正解决问题。所以,一定要学会查看日志,养成检查日志的习惯,对提升你的数据库应用开发能力至关重要。

MySQL8.0 官网日志地址:“ MySQL :: MySQL 8.0 Reference Manual :: 5.4 MySQL Server Logs

5.1 Mysql支持的日志

5.1.1 日志类型

MySQL有不同类型的日志文件,用来存储不同类型的日志,分为二进制日志、错误日志、 通用查询日志和慢查询日志,这也是常用的4种。MySQL 8又新增两种支持的日志:中继日志和数据定义语句日志 。使用这些日志文件,可以查看MySQL内部发生的事情。

这6类日志分别为:

  • 慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便我们对查询进行优化。
  • 通用查询日志:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。
  • 错误日志:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便我们了解服务器的状态,从而对服务器进行维护。
  • 二进制日志:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。
  • 中继日志:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。
  • 数据定义语句日志:记录数据定义语句执行的元数据操作。除二进制日志外,其他日志都是文本文件 。默认情况下,所有日志创建于 MySQL数据目录中。

5.1.2 日志的弊端

  • 日志功能会降低MySQL数据库的性能 。
  • 日志会占用大量的磁盘空间 。

5.2 慢查询日志

前面章节《性能分析工具的使用》已经详细讲述。

5.3 通用查询日志

通用查询日志用来记录用户的所有操作 ,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL数据库服务器的所有SQL指令等。当我们的数据发生异常时,查看通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。

5.3.1 问题场景

在电商系统中,购买商品并且使用微信支付完成以后,却发现支付中心的记录并没有新增,此时用户再次使用支付宝支付,就会出现重复支付的问题。但是当去数据库中查询数据的时候,会发现只有一条记录存在。那么此时给到的现象就是只有一条支付记录,但是用户却支付了两次。

我们对系统进行了仔细检查,没有发现数据问题,因为用户编号和订单编号以及第三方流水号都是对的。可是用户确实支付了两次,这个时候,我们想到了检查通用查询日志,看看当天到底发生了什么。

查看之后,发现:1月1日下午2点,用户使用微信支付完以后,但是由于网络故障,支付中心没有及时收到微信支付的回调通知,导致当时没有写入数据。1月1日下午2点30,用户又使用支付宝支付,此时记录更新到支付中心。1月1日晚上 9点,微信的回调通知过来了,但是支付中心已经存在了支付宝的记录,所以只能覆盖记录了。

由于网络的原因导致了重复支付。至于解决问题的方案就很多了,这里省略。

可以看到通用查询日志可以帮助我们了解操作发生的具体时间和操作的细节,对找出异常发生的原因极其关键。

5.3.2 查看当前状态

mysql> SHOW VARIABLES LIKE '%general%';
+------------------+------------------------------+
| Variable_name | Value |
+------------------+------------------------------+
| general_log | OFF | #通用查询日志处于关闭状态
| general_log_file | /var/lib/mysql/atguigu01.log | #通用查询日志文件的名称是atguigu01.log
+------------------+------------------------------+
2 rows in set (0.03 sec)

5.3.3 启动日志

方式1:永久性方式

修改my.cnf或者my.ini配置文件来设置。在[mysqld]组下加入log选项,并重启MySQL服务。格式如下:

[mysqld]
general_log=ON
general_log_file=[path[filename]] #日志文件所在目录路径,filename为日志文件名

如果不指定目录和文件名,通用查询日志将默认存储在MySQL数据目录中的hostname.log文件中,hostname表示主机名。

方式2:临时性方式

SET GLOBAL general_log=on; # 开启通用查询日志
SET GLOBAL general_log_file=’path/filename’; # 设置日志文件保存位置

5.3.4 查看日志

通用查询日志是以文本文件的形式存储在文件系统中的,可以使用文本编辑器直接打开日志文件。每台MySQL服务器的通用查询日志内容是不同的。

在通用查询日志里面,我们可以清楚地看到,什么时候开启了新的客户端登陆数据库,登录之后做了什么 SQL 操作,针对的是哪个数据表等信息。

5.3.5 停止日志

方式1:永久性方式

修改 my.cnf 或者 my.ini 文件,把[mysqld]组下的 general_log 值设置为 OFF 或者把general_log一项注释掉。修改保存后,再 重启MySQL服务 ,即可生效。 举例1:

[mysqld]
general_log=OFF
#general_log=ON

方式2:临时性方式

使用SET语句停止MySQL通用查询日志功能:

SET GLOBAL general_log=off;

5.3.6 删除、刷新日志

如果数据的使用非常频繁,那么通用查询日志会占用服务器非常大的磁盘空间。数据管理员可以删除很长时间之前的查询日志,以保证MySQL服务器上的硬盘空间。

手动删除文件

SHOW VARIABLES LIKE '%general_log%';

可以看出,通用查询日志的目录默认为MySQL数据目录。在该目录下手动删除通用查询日志atguigu01.log。

刷新日志

使用如下命令重新生成查询日志文件,具体命令如下。刷新MySQL数据目录,发现创建了新的日志文件。前提一定要开启通用日志。

mysqladmin -uroot -p flush-logs

5.4 错误日志

5.4.1 启动日志

在MySQL数据库中,错误日志功能是默认开启的。而且,错误日志无法被禁止 。

默认情况下,错误日志存储在MySQL数据库的数据文件夹下,名称默认为 mysqld.log (Linux系统)或hostname.err (mac系统)。如果需要制定文件名,则需要在my.cnf或者my.ini中做如下配置:

[mysqld]
log-error=[path/[filename]] #path为日志文件所在的目录路径,filename为日志文件名

修改配置项后,需要重启MySQL服务以生效。

5.4.2 查看日志

MySQL错误日志是以文本文件形式存储的,可以使用文本编辑器直接查看。

查询错误日志的存储路径:

mysql> SHOW VARIABLES LIKE 'log_err%';
+----------------------------+----------------------------------------+
| Variable_name | Value |
+----------------------------+----------------------------------------+
| log_error | /var/log/mysqld.log |
| log_error_services | log_filter_internal; log_sink_internal |
| log_error_suppression_list | |
| log_error_verbosity | 2 |
+----------------------------+----------------------------------------+
4 rows in set (0.01 sec)

执行结果中可以看到错误日志文件是mysqld.log,位于MySQL默认的数据目录下。

5.4.3 删除、刷新日志

对于很久以前的错误日志,数据库管理员查看这些错误日志的可能性不大,可以将这些错误日志删除,以保证MySQL服务器上的硬盘空间 。

MySQL的错误日志是以文本文件的形式存储在文件系统中的,可以直接删除 。

[root@atguigu01 log]# mysqladmin -uroot -p flush logs

5.5 二进制日志

binlog可以说是MySQL中比较重要的日志了,在日常开发及运维过程中,经常会遇到。

binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的DDL和DML等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)。

binlog主要应用场景:

  • 一是用于数据恢复
  • 二是用于数据复制

 5.5.1 查看默认情况

查看记录二进制日志是否开启:在MySQL8中默认情况下,二进制文件是开启的。

mysql> show variables like '%log_bin%';
+---------------------------------+------------------------------------+
| Variable_name                   | Value                              |
+---------------------------------+------------------------------------+
| log_bin                         | ON                                 |
| log_bin_basename                | /usr/local/mysql/data/binlog       |
| log_bin_index                   | /usr/local/mysql/data/binlog.index |
| log_bin_trust_function_creators | OFF                                |
| log_bin_use_v1_row_events       | OFF                                |
| sql_log_bin                     | ON                                 |
+---------------------------------+------------------------------------+
6 rows in set (0.00 sec)

5.5.2 日志参数设置

方式1:永久性方式

修改MySQL的my.cnf或my.ini文件可以设置二进制日志的相关参数:

[mysqld]
#启用二进制日志
log-bin=binlog
binlog_expire_logs_seconds=600
max_binlog_size=100M

重新启动MySQL服务,查询二进制日志的信息.

设置带文件夹的bin-log日志存放目录

如果想改变日志文件的目录和名称,可以对my.cnf或my.ini中的log_bin参数修改如下:

[mysqld]
log-bin="/var/lib/mysql/binlog/atguigu-bin"

注意:新建的文件夹需要使用mysql用户,使用下面的命令即可。

chown -R -v mysql:mysql binlog

方式2:临时性方式

如果不希望通过修改配置文件并重启的方式设置二进制日志的话,还可以使用如下指令,需要注意的是在mysql8中只有会话级别的设置,没有了global级别的设置。

# global 级别
mysql> set global sql_log_bin=0;
ERROR 1228 (HY000): Variable 'sql_log_bin' is a SESSION variable and can`t be used with SET GLOBAL
# session级别
mysql> SET sql_log_bin=0;
Query OK, 0 rows affected (0.01 秒)

5.5.3 查看日志

当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。

MySQL服务重新启动一次 ,以“.000001”为后缀的文件就会增加一个,并且后缀名按1递增。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过max_binlog_size 的上限(默认是1GB),就会创建一个新的日志文件。

查看当前的二进制日志文件列表及大小。指令如下:

mysql> SHOW BINARY LOGS;
+---------------+-----------+-----------+
| Log_name      | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000075 |       180 | No        |
| binlog.000076 |       180 | No        |
| binlog.000077 |       157 | No        |
| binlog.000078 |       180 | No        |
| binlog.000079 |       619 | No        |
| binlog.000080 |       180 | No        |
| binlog.000081 |      2764 | No        |
| binlog.000082 |       512 | No        |
| binlog.000083 |       180 | No        |
| binlog.000084 |       789 | No        |
| binlog.000085 |       180 | No        |
| binlog.000086 |       180 | No        |
| binlog.000087 |       157 | No        |
+---------------+-----------+-----------+
13 rows in set (0.00 sec)

下面命令将行事件以 伪SQL的形式 表现出来:

mysqlbinlog -v "/usr/local/mysql/data/binlog.000081"

 前面的命令同时显示binlog格式的语句,使用如下命令不显示它

mysqlbinlog -v --base64-output=DECODE-ROWS "/usr/local/mysql/data/binlog.000081"

关于mysqlbinlog工具的使用技巧还有很多,例如只解析对某个库的操作或者某个时间段内的操作等。简单分享几个常用的语句,更多操作可以参考官方文档。

# 可查看参数帮助
mysqlbinlog --no-defaults --help
# 查看最后100行
mysqlbinlog --no-defaults --base64-output=decode-rows -vv binlog.000081 |tail -100
# 根据position查找
mysqlbinlog --no-defaults --base64-output=decode-rows -vv binlog.000081 |grep -A 20 '2591'

上面这种办法读取出binlog日志的全文内容比较多,不容易分辨查看到pos点信息,下面介绍一种更为方便的查询命令:

mysql> show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];
  • IN 'log_name' :指定要查询的binlog文件名(不指定就是第一个binlog文件) 
  • FROM pos :指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算)
  • LIMIT [offset] :偏移量(不指定就是0)
  • row_count :查询总条数(不指定就是所有行)
mysql> show binlog events IN 'binlog.000081' from 2591;
+---------------+------+-------------+-----------+-------------+---------------------------------+
| Log_name      | Pos  | Event_type  | Server_id | End_log_pos | Info                            |
+---------------+------+-------------+-----------+-------------+---------------------------------+
| binlog.000081 | 2591 | Table_map   |         1 |        2648 | table_id: 222 (atguigudb.test)  |
| binlog.000081 | 2648 | Update_rows |         1 |        2710 | table_id: 222 flags: STMT_END_F |
| binlog.000081 | 2710 | Xid         |         1 |        2741 | COMMIT /* xid=339 */            |
| binlog.000081 | 2741 | Stop        |         1 |        2764 |                                 |
+---------------+------+-------------+-----------+-------------+---------------------------------+
4 rows in set (0.00 sec)

上面我们讲了这么多都是基于binlog的默认格式,binlog格式查看

mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.00 sec)

除此之外,binlog还有2种格式,分别是Statement和Mixed

  • Statement
    • 每一条会修改数据的sql都会记录在binlog中。
    • 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
  • Row
    • 5.1.5版本的MySQL才开始支持row level 的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。
    • 优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。
  • Mixed
    • 从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

5.5.4 使用日志恢复数据

mysqlbinlog恢复数据的语法如下:

mysqlbinlog [option] filename|mysql –uuser -ppass;

mysqlbinlog --start-date='2023-03-15 15:00:00'  --stop-date='2023-03-15 15:00:00' /usr/local/mysql/data/binlog.000081 | mysql -uroot -p 

这个命令可以这样理解:使用mysqlbinlog命令来读取filename中的内容,然后使用mysql命令将这些内容恢复到数据库中。

  • filename :是日志文件名。
  • option :可选项,比较重要的两对option参数是--start-date、--stop-date 和 --start-position、--stop-position。
    • --start-date 和 --stop-date :可以指定恢复数据库的起始时间点和结束时间点。
    • --start-position和--stop-position :可以指定恢复数据的开始位置和结束位置。

注意:使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如binlog.000001必须在binlog.000002之前恢复。

5.5.5 删除二进制日志

MySQL的二进制文件可以配置自动删除,同时MySQL也提供了安全的手动删除二进制文件的方法。

PURGE MASTER LOGS 只删除指定部分的二进制日志文件,

RESET MASTER 删除所有的二进制日志文件。

1、PURGE MASTER LOGS:删除指定日志文件

PURGE MASTER LOGS语法如下:

PURGE {MASTER | BINARY} LOGS TO ‘指定日志文件名’;
PURGE {MASTER | BINARY} LOGS BEFORE ‘指定日期’;

2、RESET MASTER: 删除所有二进制日志文件

慎用!

RESET MASTER;

执行完该语句后,原来的所有二进制日志已经全部被删除。

5.5.6 其他场景

二进制日志可以通过数据库的全量备份和二进制日志中保存的增量信息 ,完成数据库的 无损失恢复 。但是,如果遇到数据量大、数据库和数据表很多(比如分库分表的应用)的场景,用二进制日志进行数据恢复,是很有挑战性的,因为起止位置不容易管理。在这种情况下,一个有效的解决办法是配置主从数据库服务器 ,甚至是一主多从的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。

5.6 再谈二进制日志

5.6.1 写入机制

binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache ,事务提交的时候,再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。

 write和fsync的时机,可以由参数sync_binlog控制,默认是0。为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。

虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。如下图:

 为了安全起见,可以设置为 1 ,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样。

最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。

 在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

5.6.2 binlog与redolog对比

  • redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎层产生的。
  • 而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。

5.6.3 两阶段提交

在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。

 redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?

 由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。

 为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。

 使用两阶段提交后,写入binlog时发生异常也不会有影响

 另一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?

 并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。

5.7 中继日志

5.7.1 介绍

中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志 。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步 。

搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。

文件名的格式是: 从服务器名-relay-bin.序号。中继日志还有一个索引文件: 从服务器名-relaybin.index,用来定位当前正在使用的中继日志。

5.7.2 查看中继日志

中继日志与二进制日志的格式相同,可以用mysqlbinlog工具进行查看。下面是中继日志的一个片段:

SET TIMESTAMP=1618558728/*!*/;
BEGIN
/*!*/;
# at 950
#210416 15:38:48 server id 1 end_log_pos 832 CRC32 0xcc16d651 Table_map:
`atguigu`.`test` mapped to number 91
# at 1000
#210416 15:38:48 server id 1 end_log_pos 872 CRC32 0x07e4047c Delete_rows: table id
91 flags: STMT_END_F -- server id 1 是主服务器,意思是主服务器删了一行数据
BINLOG '
CD95YBMBAAAAMgAAAEADAAAAAFsAAAAAAAEABGRlbW8ABHRlc3QAAQMAAQEBAFHWFsw=
CD95YCABAAAAKAAAAGgDAAAAAFsAAAAAAAEAAgAB/wABAAAAfATkBw==
'/*!*/;
# at 1040

这一段的意思是,主服务器(“server id 1”)对表atguigu.test进行了2步操作:

定位到表 atguigu.test 编号是 91 的记录,日志位置是 832;
删除编号是 91 的记录,日志位置是 872。

5.7.3 恢复的典型错误

如果从服务器宕机,有的时候为了系统恢复,要重装操作系统,这样就可能会导致你的服务器名称与之前不同 。而中继日志里是包含从服务器名的。在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是名称不对了。

解决的方法也很简单,把从服务器的名称改回之前的名称。

六、主从复制

6.1 主从复制概述

6.1.2 如何提升数据库并发能力

  • 考虑将热点数据缓存:

  • 做主从架构 、进行读写分离。

一般应用对数据库而言都是“ 读多写少 ”,也就说对数据库读取数据的压力比较大,有一个思路就是采用数据库集群的方案,做主从架构 、进行读写分离 ,这样同样可以提升数据库的并发处理能力。

但并不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的。

如果目的在于提升数据库高并发访问的效率,那么首先考虑的是如何优化SQL和索引 ,这种方式简单有效;其次才是采用缓存的策略 ,比如使用 Redis将热点数据保存在内存数据库中,提升读取的效率;最后才是对数据库采用主从架构 ,进行读写分离。

6.1.3 主从复制的作用

主从同步设计不仅可以提高数据库的吞吐量,还有以下 3 个方面的作用。

  • 读写分离。
  • 数据备份。
  • 具有高可用性。

6.2 主从复制原理

Slave 会从 Master 读取 binlog 来进行数据同步。

6.2.1 原理剖析

6.2.1.1三个线程

实际上主从同步的原理就是基于binlog进行数据同步的。在主从复制过程中,会基于3个线程来操作,一个主库线程,两个从库线程。

 二进制日志转储线程(Binlog dump thread)是一个主库线程。当从库线程连接的时候, 主库可以将二进制日志发送给从库,当主库读取事件(Event)的时候,会在Binlog上加锁 ,读取完成之后,再将锁释放掉。

从库I/O 线程会连接到主库,向主库发送请求更新Binlog。这时从库的I/O线程就可以读取到主库的二进制日志转储线程发送的Binlog更新部分,并且拷贝到本地的中继日志 (Relay log)。

从库SQL线程 会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步。

6.2.1.2复制三步骤

  • 步骤1:Master将写操作记录到二进制日志( binlog )。
  • 步骤2:Slave将Master的binary log events拷贝到它的中继日志( relay log );
  • 步骤3:Slave重做中继日志中的事件,将改变应用到自己的数据库中。 MySQL复制是异步的且串行化的,而且重启后从接入点开始复制。

6.2.1.3复制的问题

复制的最大问题: 延时

6.2.2 复制的基本原则

  • 每个Slave只有一个Master
  • 每个Slave只能有一个唯一的服务器ID
  • 每个Master可以有多个Slave

6.3 一主一从架构搭建

一台主机用于处理所有写请求 ,一台从机负责所有读请求 ,架构图如下:

6.3.1 准备工作

1、准备 2台 CentOS 虚拟机

2、每台虚拟机上需要安装好MySQL (可以是MySQL8.0 )

注意:克隆的方式需要修改新克隆出来主机的:① MAC地址 ② hostname ③ IP 地址 ④ UUID 。

此外,克隆的方式生成的虚拟机(包含MySQL Server),则克隆的虚拟机MySQL Server的UUID相同,必须修改,否则在有些场景会报错。比如: show slave status\G ,报如下的错误:

Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave haveequal MySQL server UUIDs; these UUIDs must be different for replication to work.

修改MySQL Server 的UUID方式:

vim /var/lib/mysql/auto.cnf systemctl restart mysqld

6.3.2 主机配置文件

建议mysql版本一致且后台以服务运行,主从所有配置项都配置在 [mysqld] 节点下,且都是小写字母。

具体参数配置如下:

  • 必选
#[必须]主服务器唯一ID
server-id=1

#[必须]启用二进制日志,指名路径。比如:自己本地的路径/log/mysqlbin
log-bin=bin-log
  • 可选
#[可选] 0(默认)表示读写(主机),1表示只读(从机)
read-only=0
#设置日志文件保留的时长,单位是秒
binlog_expire_logs_seconds=6000
#控制单个二进制日志大小。此参数的最大和默认值是1GB
max_binlog_size=200M
#[可选]设置不要复制的数据库
binlog-ignore-db=test
#[可选]设置需要复制的数据库,默认全部记录。
binlog-do-db=需要复制的主数据库名字
#[可选]设置binlog格式
binlog_format=STATEMENT

binlog格式设置:

1、STATEMENT模式 (基于SQL语句的复制(statement-based replication, SBR))

binlog_format=STATEMENT

每一条会修改数据的sql语句会记录到binlog中。这是默认的binlog格式。

  • SBR 的优点:
    • 历史悠久,技术成熟
    • 不需要记录每一行的变化,减少了binlog日志量,文件较小
    • binlog中包含了所有数据库更改信息,可以据此来审核数据库的安全等情况
    • binlog可以用于实时的还原,而不仅仅用于复制
    • 主从版本可以不一样,从服务器版本可以比主服务器版本高
  • SBR 的缺点:
    • 不是所有的UPDATE语句都能被复制,尤其是包含不确定操作的时候
  • 使用以下函数的语句也无法被复制:LOAD_FILE()、UUID()、USER()、FOUND_ROWS()、SYSDATE()(除非启动时启用了 --sysdate-is-now 选项)
  • INSERT ... SELECT 会产生比 RBR 更多的行级锁
  • 复制需要进行全表扫描(WHERE 语句中没有使用到索引)的 UPDATE 时,需要比 RBR 请求更多的行级锁
  • 对于有 AUTO_INCREMENT 字段的 InnoDB表而言,INSERT 语句会阻塞其他 INSERT 语句
  • 对于一些复杂的语句,在从服务器上的耗资源情况会更严重,而 RBR 模式下,只会对那个发生变化的记录产生影响
  • 执行复杂语句如果出错的话,会消耗更多资源
  • 数据表必须几乎和主服务器保持一致才行,否则可能会导致复制出错

2、ROW模式(基于行的复制(row-based replication, RBR))

binlog_format=ROW
  • RBR 的优点:
    • 任何情况都可以被复制,这对复制来说是最安全可靠的。(比如:不会出现某些特定情况下的存储过程、function、trigger的调用和触发无法被正确复制的问题)
    • 多数情况下,从服务器上的表如果有主键的话,复制就会快了很多
    • 复制以下几种语句时的行锁更少:INSERT ... SELECT、包含 AUTO_INCREMENT 字段的 INSERT、没有附带条件或者并没有修改很多记录的 UPDATE 或 DELETE 语句
    • 执行 INSERT,UPDATE,DELETE 语句时锁更少
    • 从服务器上采用多线程来执行复制成为可能
  • RBR 的缺点:
    • binlog 大了很多
    • 复杂的回滚时 binlog 中会包含大量的数据
    • 主服务器上执行UPDATE语句时,所有发生变化的记录都会写到binlog中,而 SBR 只会写一次,这会导致频繁发生 binlog 的并发写问题
    • 无法从binlog中看到都复制了些什么语句

3、MIXED模式(混合模式复制(mixed-based replication, MBR))

binlog_format=MIXED

从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

binlog_format=STATEMENTbinlog_format=ROWbinlog_format=MIXED在Mixed模式下,一般的语句修改使用statment格式保存binlog。

如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog。MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。

6.3.3 从机配置文件

要求主从所有配置项都配置在 my.cnf 的 [mysqld] 栏位下,且都是小写字母。

  • 必选
#[必须]从服务器唯一ID
server-id=2
  • 可选
#[可选]启用中继日志
relay-log=mysql-relay

重启后台mysql服务,使配置生效。

注意:主从机都关闭防火墙

service iptables stop #CentOS 6

systemctl stop firewalld.service #CentOS 7

6.3.4 主机:建立账户并授权

#5.5,5.7
#在主机MySQL里执行授权主从复制的命令
GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'从机器数据库IP' IDENTIFIED BY 'abc123';

注意:如果使用的是MySQL8,需要如下的方式建立账户,并授权slave:

CREATE USER 'slave1'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'%';
#此语句必须执行。否则见下面。
ALTER USER 'slave1'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
flush privileges;

注意:在从机执行show slave status\G时报错:

Last_IO_Error: error connecting to master '[email protected]:3306' - retry-time: 60 retries: 1

message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.

查询Master的状态,并记录下File和Position的值。

show master status;

6.3.5 从机:配置需要复制的主机

步骤1:从机上复制主机的命令

CHANGE MASTER TO
MASTER_HOST='主机的IP地址',
MASTER_USER='主机用户名',
MASTER_PASSWORD='主机用户名的密码',
MASTER_LOG_FILE='mysql-bin.具体数字',
MASTER_LOG_POS=具体值;

举例:

CHANGE MASTER TO
MASTER_HOST='192.168.1.150',MASTER_USER='slave1',MASTER_PASSWORD='123456',MASTER_LOG_F
ILE='atguigu-bin.000007',MASTER_LOG_POS=154;

步骤2:

#启动slave同步
START SLAVE;

 如果报错:

 可以执行如下操作,删除之前的relay_log信息。然后重新执行 CHANGE MASTER TO ...语句即可。

mysql> reset slave; 
#删除SLAVE数据库的relaylog日志文件,并重新启用新的relaylog文件

接着,查看同步状态:

SHOW SLAVE STATUS\G;

 显式如下的情况,就是不正确的。可能错误的原因有:

  • 网络不通
  • 账户密码错误
  • 防火墙
  • mysql配置文件问题
  • 连接服务器时语法
  • 主服务器mysql权限

6.3.6 测试

主机新建库、新建表、insert记录,从机复制:

CREATE DATABASE atguigu_master_slave;
CREATE TABLE mytbl(id INT,NAME VARCHAR(16));
INSERT INTO mytbl VALUES(1, 'zhang3');
INSERT INTO mytbl VALUES(2,@@hostname);

6.3.7 停止主从同步

  • 停止主从同步命令
stop slave;
  • 如何重新配置主从

如果停止从服务器复制功能,再使用需要重新配置主从。

重新配置主从,需要在从机上执行:

stop slave;
reset master; 
#删除Master中所有的binglog文件,并将日志索引文件清空,重新开始所有新的日志文件(慎用)

6.3.8 后续

搭建主从复制:双主双从

6.4 同步数据一致性问题

主从同步的要求:

  • 读库和写库的数据一致(最终一致);
  • 写数据必须写到写库;
  • 读数据必须到读库(不一定);

6.4.1 理解主从延迟问题

进行主从同步的内容是二进制日志,它是一个文件,在进行网络传输的过程中就一定会存在主从延迟(比如 500ms),这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的数据不一致性问题。

6.4.2 主从延迟问题原因

在网络正常的时候,日志从主库传给从库所需的时间是很短的,即T2-T1的值是非常小的。即,网络正常情况下,主备延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差。主备延迟最直接的表现是,从库消费中继日志(relay log)的速度,比主库生产binlog的速度要慢。造成原因:

1、从库的机器性能比主库要差

2、从库的压力大

3、大事务的执行

举例1:一次性用delete语句删除太多数据结论:后续再删除数据的时候,要控制每个事务删除的数据量,分成多次删除。

举例2:一次性用insert...select插入太多数据

举例3:大表DDL

比如在主库对一张500W的表添加一个字段耗费了10分钟,那么从节点上也会耗费10分钟。

6.4.3 如何减少主从延迟

若想要减少主从延迟的时间,可以采取下面的办法:

  1. 降低多线程大事务并发的概率,优化业务逻辑
  2. 优化SQL,避免慢SQL, 减少批量操作 ,建议写脚本以update-sleep这样的形式完成。
  3. 提高从库机器的配置 ,减少主库写binlog和从库读binlog的效率差。
  4. 尽量采用短的链路 ,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输的网络延时。
  5. 实时性要求的业务读强制走主库,从库只做灾备,备份。

6.4.4 如何解决一致性问题

如果操作的数据存储在同一个数据库中,那么对数据进行更新的时候,可以对记录加写锁,这样在读取的时候就不会发生数据不一致的情况。但这时从库的作用就是备份 ,并没有起到读写分离 ,分担主库读压力的作用。

 读写分离情况下,解决主从同步中数据不一致的问题, 就是解决主从之间数据复制方式的问题,如果按照数据一致性从弱到强来进行划分,有以下 3 种复制方式。

方法 1:异步复制

 方法 2:半同步复制

 方法 3:组复制

异步复制和半同步复制都无法最终保证数据的一致性问题,半同步复制是通过判断从库响应的个数来决定是否返回给客户端,虽然数据一致性相比于异步复制有提升,但仍然无法满足对数据一致性要求高的场景,比如金融领域。MGR 很好地弥补了这两种复制模式的不足。组复制技术,简称 MGR(MySQL Group Replication)。是 MySQL 在 5.7.17 版本中推出的一种新的数据复制技术,这种复制技术是基于 Paxos 协议的状态机复制。

MGR 是如何工作的

首先我们将多个节点共同组成一个复制组,在 执行读写(RW)事务的时候,需要通过一致性协议层(Consensus 层)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应 Node 节点)的同意,大多数指的是同意的节点数量需要大于 (N/2+1),这样才可以进行提交,而不是原发起方一个说了算。而针对只读(RO)事务 则不需要经过组内同意,直接COMMIT即可。在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据的一致性。

 MGR 将MySQL带入了数据强一致性的时代,是一个划时代的创新,其中一个重要的原因就是MGR 是基于Paxos协议的。Paxos算法是由Leslie Lamport于1990年提出的,有关这个算法的决策机制可以搜一下。事实上,Paxos算法提出来之后就作为分布式一致性算法被广泛应用,比如Apache的 ZooKeeper 也是基于Paxos实现的。

6.5 知识延伸

在主从架构的配置中,如果想要采取读写分离的策略,我们可以自己编写程序 ,也可以通过第三方的中间件来实现。

  • 自己编写程序的好处就在于比较自主,我们可以自己判断哪些查询在从库上来执行,针对实时性要求高的需求,我们还可以考虑哪些查询可以在主库上执行。同时,程序直接连接数据库,减少了中间件层,相当于减少了性能损耗。
  • 采用中间件的方法有很明显的优势, 功能强大 , 使用简单 。但因为在客户端和数据库之间增加了中间件层会有一些性能损耗 ,同时商业中间件也是有使用成本的。我们也可以考虑采取一些优秀的开源工具。

  1. Cobar 属于阿里B2B事业群,始于2008年,在阿里服役3年多,接管3000+个MySQL数据库的schema,集群日处理在线SQL请求50亿次以上。由于Cobar发起人的离职,Cobar停止维护。
  2. Mycat 是开源社区在阿里cobar基础上进行二次开发,解决了cobar存在的问题,并且加入了许多新的功能在其中。青出于蓝而胜于蓝。
  3. OneProxy 基于MySQL官方的proxy思想利用c语言进行开发的,OneProxy是一款商业 收费的中间件。舍弃了一些功能,专注在性能和稳定性上 。
  4. kingshard 由小团队用go语言开发,还需要发展,需要不断完善。
  5. Vitess 是Youtube生产在使用,架构很复杂。不支持MySQL原生协议,使用需要大量改造成本 。
  6. Atlas 是360团队基于mysql proxy改写,功能还需完善,高并发下不稳定。
  7. MaxScale 是mariadb(MySQL原作者维护的一个版本)研发的中间件
  8. MySQLRoute 是MySQL官方Oracle公司发布的中间件

 主备切换:

七、数据库备份与恢复

7.1 物理备份和逻辑备份

物理备份:备份数据文件,转储数据库物理文件到某一目录。物理备份恢复速度比较快,但占用空间比较大,MySQL中可以用xtrabackup工具来进行物理备份。

逻辑备份:对数据库对象利用工具进行导出工作,汇总入备份文件内。逻辑备份恢复速度慢,但占用空间小,更灵活。MySQL 中常用的逻辑备份工具为mysqldump 。逻辑备份就是备份sql语句 ,在恢复的时候执行备份的sql语句实现数据库数据的重现。

7.2 mysqldump实现逻辑备份

7.2.1 备份一个数据库

基本语法:

mysqldump –u 用户名称 –h 主机名称 –p密码 待备份的数据库名称[tbname, [tbname...]]> 备份文件名称.sql

举例:

mysqldump -uroot -p xiang > test.sql #备份文件存储在当前目录下
mysqldump -uroot -p xiang > /var/lib/mysql/xiang.sql

7.2.2 备份全部数据库

若想用mysqldump备份整个实例,可以使用 --all-databases 或 -A 参数:

mysqldump -uroot -pxxxxxx --all-databases > all_database.sql
mysqldump -uroot -pxxxxxx -A > all_database.sql

7.2.3 备份部分数据库

使用 --databases 或 -B 参数了,该参数后面跟数据库名称,多个数据库间用空格隔开。如果指定databases参数,备份文件中会存在创建数据库的语句,如果不指定参数,则不存在。语法如下:

mysqldump –u user –h host –p --databases [数据库的名称1 [数据库的名称2...]] > 备份文件名称.sql

举例:

mysqldump -uroot -p --databases xiang test > two_database.sql
mysqldump -uroot -p -B xiang test > two_database.sql

如果不携带--database,则仅支持导出一个数据库

7.2.4 备份部分表

比如,在表变更前做个备份。语法如下:

mysqldump –u user –h host –p 数据库的名称 [表名1 [表名2...]] > 备份文件名称.sql

举例:

mysqldump -uroot -p xiang test > test2.sql
mysqldump -uroot -p xiang test emp1 > test2.sql

7.2.5 备份单表的部分数据

有些时候一张表的数据量很大,我们只需要部分数据。这时就可以使用 --where 选项了。where后面附带需要满足的条件。

举例:备份student表中id小于10的数据:

mysqldump -uroot -p xiang student --where="id < 10 " > student_part_id10_low_bak.sql

7.2.6 排除某些表的备份

如果我们想备份某个库,但是某些表数据量很大或者与业务关联不大,这个时候可以考虑排除掉这些表,同样的,选项 --ignore-table 可以完成这个功能。

mysqldump -uroot -p -B test xiang --ignore-table=test.student > no_stu_bak.sql

通过如下指定判定文件中没有student表结构:

grep "student" no_stu_bak.sql

7.2.7 只备份结构或只备份数据

只备份结构的话可以使用 --no-data 简写为 -d 选项;只备份数据可以使用 --no-create-info 简写为-t 选项。

# 只备份结构
mysqldump -uroot -p xiang --no-data > no_data_bak.sql

# 只备份数据
mysqldump -uroot -p xiang --no-create-info > no_create_info_bak.sql

7.2.8 备份中包含存储过程、函数、事件

mysqldump备份默认是不包含存储过程,自定义函数及事件的。可以使用 --routines 或 -R 选项来备份存储过程及函数,使用 --events 或 -E 参数来备份事件。

举例:备份整个库,包含存储过程及事件:

使用下面的SQL可以查看当前库有哪些存储过程或者函数

SELECT SPECIFIC_NAME,ROUTINE_TYPE ,ROUTINE_SCHEMA FROM information_schema.Routines WHERE ROUTINE_SCHEMA="xiang";

下面备份atguigu库的数据,函数以及存储过程。

mysqldump -uroot -p -R -E --databases xiang > full_bak.sql

7.2.9 mysqldump常用选项

--add-drop-database:在每个CREATE DATABASE语句前添加DROP DATABASE语句。
--add-drop-tables:在每个CREATE TABLE语句前添加DROP TABLE语句。
--add-locking:用LOCK TABLES和UNLOCK TABLES语句引用每个表转储。重载转储文件时插入得更快。
--all-database, -A:转储所有数据库中的所有表。与使用--database选项相同,在命令行中命名所有数据库。
--comment[=0|1]:如果设置为0,禁止转储文件中的其他信息,例如程序版本、服务器版本和主机。--skipcomments与--comments=0的结果相同。默认值为1,即包括额外信息。
--compact:产生少量输出。该选项禁用注释并启用--skip-add-drop-tables、--no-set-names、--skipdisable-keys和--skip-add-locking选项。
--compatible=name:产生与其他数据库系统或旧的MySQL服务器更兼容的输出,值可以为ansi、MySQL323、MySQL40、postgresql、oracle、mssql、db2、maxdb、no_key_options、no_table_options或者no_field_options。
--complete_insert, -c:使用包括列名的完整的INSERT语句。
--debug[=debug_options], -#[debug_options]:写调试日志。
--delete,-D:导入文本文件前清空表。
--default-character-set=charset:使用charsets默认字符集。如果没有指定,就使用utf8。
--delete--master-logs:在主复制服务器上,完成转储操作后删除二进制日志。该选项自动启用-masterdata。
--extended-insert,-e:使用包括几个VALUES列表的多行INSERT语法。这样使得转储文件更小,重载文件时可以加速插入。
--flush-logs,-F:开始转储前刷新MySQL服务器日志文件。该选项要求RELOAD权限。
--force,-f:在表转储过程中,即使出现SQL错误也继续。
--lock-all-tables,-x:对所有数据库中的所有表加锁。在整体转储过程中通过全局锁定来实现。该选项自动关闭--single-transaction和--lock-tables。
--lock-tables,-l:开始转储前锁定所有表。用READ LOCAL锁定表以允许并行插入MyISAM表。对于事务表(例如InnoDB和BDB),--single-transaction是一个更好的选项,因为它根本不需要锁定表。
--no-create-db,-n:该选项禁用CREATE DATABASE /*!32312 IF NOT EXIST*/db_name语句,如果给出--database或--all-database选项,就包含到输出中。
--no-create-info,-t:只导出数据,而不添加CREATE TABLE语句。
--no-data,-d:不写表的任何行信息,只转储表的结构。
--opt:该选项是速记,它可以快速进行转储操作并产生一个能很快装入MySQL服务器的转储文件。该选项默认开启,但可以用--skip-opt禁用。
--password[=password],-p[password]:当连接服务器时使用的密码。-port=port_num,-P port_num:用于连接的TCP/IP端口号。
--protocol={TCP|SOCKET|PIPE|MEMORY}:使用的连接协议。
--replace,-r –replace和--ignore:控制替换或复制唯一键值已有记录的输入记录的处理。如果指定--replace,新行替换有相同的唯一键值的已有行;如果指定--ignore,复制已有的唯一键值的输入行被跳过。如果不指定这两个选项,当发现一个复制键值时会出现一个错误,并且忽视文本文件的剩余部分。
--silent,-s:沉默模式。只有出现错误时才输出。
--socket=path,-S path:当连接localhost时使用的套接字文件(为默认主机)。
--user=user_name,-u user_name:当连接服务器时MySQL使用的用户名。
--verbose,-v:冗长模式,打印出程序操作的详细信息。
--xml,-X:产生XML输出。

7.3 mysql命令恢复数据

基本语法:

mysql –u root –p [dbname] < backup.sql

7.3.1 单库备份中恢复单库

使用root用户,将之前练习中备份的atguigu.sql文件中的备份导入数据库中,命令如下:如果备份文件中包含了创建数据库的语句,则恢复的时候不需要指定数据库名称,如下所示

mysql -uroot -p < test.sql

否则需要指定数据库名称,如下所示

mysql -uroot -p xiang < test.sql

7.3.2 全量备份恢复

如果我们现在有昨天的全量备份,现在想整个恢复,则可以这样操作:

mysql –u root –p < all.sql

执行完后,MySQL数据库中就已经恢复了all.sql文件中的所有数据库。

7.3.3 从全量备份中恢复单库

可能有这样的需求,比如说我们只想恢复某一个库,但是我们有的是整个实例的备份,这个时候我们可以从全量备份中分离出单个库的备份。

举例:

sed -n '/^-- Current Database: `xiang`/,/^-- Current Database: `/p' all_database.sql> xiang.sql
#分离完成后我们再导入xiang.sql即可恢复单个库

7.3.4 从单库备份中恢复单表

这个需求还是比较常见的。比如说我们知道哪个表误操作了,那么就可以用单表恢复的方式来恢复。

举例:我们有xiang整库的备份,但是由于class表误操作,需要单独恢复出这张表。

cat xiang.sql | sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `class`/!d;q' > class_structure.sql
cat xiang.sql | grep --ignore-case 'insert into `class`' > class_data.sql
#用shell语法分离出创建表的语句及插入数据的语句后 再依次导出即可完成恢复
use xiang;
mysql> source class_structure.sql;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> source class_data.sql;
Query OK, 1 row affected (0.01 sec)

7.4 物理备份:直接复制整个数据库

直接将MySQL中的数据库文件复制出来。这种方法最简单,速度也最快。

MySQL的数据库目录位置不一定相同:

  • 在Windows平台下,MySQL 8.0存放数据库的目录通常默认为 “ C:\ProgramData\MySQL\MySQLServer 8.0\Data ”或者其他用户自定义目录;
  • 在Linux平台下,数据库目录位置通常为/var/lib/mysql/;
  • 在MAC OSX平台下,数据库目录位置通常为“/usr/local/mysql/data”

但为了保证备份的一致性。需要保证:

  1. 方式1:备份前,将服务器停止。
  2. 方式2:备份前,对相关表执行 FLUSH TABLES WITH READ LOCK 操作。这样当复制数据库目录中的文件时,允许其他客户继续查询表。同时,FLUSH TABLES语句来确保开始备份前将所有激活的索引页写入硬盘。

这种方式方便、快速,但不是最好的备份方法,因为实际情况可能 不允许停止MySQL服务器 或者 锁住表 ,而且这种方法 对InnoDB存储引擎 的表不适用。对于MyISAM存储引擎的表,这样备份和还原很方便,但是还原时最好是相同版本的MySQL数据库,否则可能会存在文件类型不同的情况。

注意,物理备份完毕后,执行 UNLOCK TABLES 来结算其他客户对表的修改行为。说明: 在MySQL版本号中,第一个数字表示主版本号,主版本号相同的MySQL数据库文件格式相同。

此外,还可以考虑使用相关工具实现备份。比如, MySQLhotcopy 工具。MySQLhotcopy是一个Perl脚本,它使用LOCK TABLES、FLUSH TABLES和cp或scp来快速备份数据库。它是备份数据库或单个表最快的途径,但它只能运行在数据库目录所在的机器上,并且只能备份MyISAM类型的表。多用于mysql5.5之前。

7.5 物理恢复:直接复制到数据库目录

步骤:

1)演示删除备份的数据库中指定表的数据

2)将备份的数据库数据拷贝到数据目录下,并重启MySQL服务器

3)查询相关表的数据是否恢复。需要使用下面的 chown 操作。

要求:

  • 必须确保备份数据的数据库和待恢复的数据库服务器的主版本号相同。因为只有MySQL数据库主版本号相同时,才能保证这两个MySQL数据库文件类型是相同的。
  • 这种方式对 MyISAM类型的表比较有效 ,对于InnoDB类型的表则不可用。因为InnoDB表的表空间不能直接复制。
  • 在Linux操作系统下,复制到数据库目录后,一定要将数据库的用户和组变成mysql,命令如下:
chown -R mysql.mysql /var/lib/mysql/dbname

其中,两个mysql分别表示组和用户;“-R”参数可以改变文件夹下的所有子文件的用户和组;“dbname”参数表示数据库目录。

提示 Linux操作系统下的权限设置非常严格。通常情况下,MySQL数据库只有root用户和mysql用户组下的mysql用户才可以访问,因此将数据库目录复制到指定文件夹后,一定要使用chown命令将文件夹的用户组变为mysql,将用户变为mysql。

7.6 表的导出和导入

7.6.1 表的导出

7.6.1.1 使用SELECT...INTO OUTFILE

在MySQL中,可以使用SELECT…INTO OUTFILE语句将表的内容导出成一个文本文件。

举例:使用SELECT…INTO OUTFILE将xiang数据库中test表中的记录导出到文本文件。

(1)选择数据库xiang,并查询test表,执行结果如下所示。

mysql> use xiang;
Database changed
mysql> select * from test;
+------+------+----------+------------+
| id   | name | salary   | hire_date  |
+------+------+----------+------------+
|    1 | Tom  | 10000.00 | 2022-06-01 |
|    3 | Tom2 | 20000.00 | 2022-06-02 |
+------+------+----------+------------+
2 rows in set (0.00 sec)

(2)mysql默认对导出的目录有权限限制,也就是说使用命令行进行导出的时候,需要指定目录进行操作。

# 查询secure_file_priv值:
mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+--------------------------+-----------------------+
| Variable_name | Value |
+--------------------------+-----------------------+
| require_secure_transport | OFF |
| secure_file_priv | /var/lib/mysql-files/ |
+--------------------------+-----------------------+
2 rows in set (0.02 sec)

(3)上面结果中显示,secure_file_priv变量的值为/var/lib/mysql-files/,导出目录设置为该目录,SQL语句如下。

SELECT * FROM test INTO OUTFILE "/var/lib/mysql-files/test.txt";

(4)查看 /var/lib/mysql-files/test.txt`文件。

7.6.1.2 使用mysqldump命令导出文本文件

举例1:使用mysqldump命令将将xiang数据库中tmp1表中的记录导出到文本文件:

mysqldump -uroot -p -T "/var/lib/mysql-files/" xiang test

mysqldump命令执行完毕后,在指定的目录/var/lib/mysql-files/下生成了test.sql和test.txt文件。

test.sql文件,其内容包含创建表的CREATE语句。打开test.txt文件,其内容只包含表中的数据。

举例2:使用mysqldump将atguigu数据库中的account表导出到文本文件,使用FIELDS选项,要求字段之间使用逗号“,”间隔,所有字符类型字段值用双引号括起来:

mysqldump -uroot -p -T "/var/lib/mysql-files/" atguigu account --fields-terminatedby=',' --fields-optionally-enclosed-by='\"'

7.6.1.3 使用mysql命令导出文本文件

举例1:使用mysql语句导出xiang数据中test表中的记录到文本文件:

mysql -uroot -p --execute="SELECT * FROM test;" xiang > "/var/lib/mysqlfiles/test31.txt"

打开test.txt文件,其内容只包含表中的数据。

举例2:将表中的记录导出到文本文件,使用--veritcal参数将该条件记录分为多行显示:

mysql -uroot -p --vertical --execute="SELECT * FROM test;" xiang > "/var/lib/mysql-files/test32.txt"

举例3:将表中的记录导出到xml文件,使用--xml参数,具体语句如下。

mysql -uroot -p --xml --execute="SELECT * FROM test;" xiang > "/var/lib/mysqlfiles/test33.xml"

7.6.2 表的导入

7.6.2.1 使用LOAD DATA IN FILE

举例1:

使用SELECT...INTO OUTFILE将xiang数据库中test表的记录导出到文本文件

SELECT * FROM xiang.test INTO OUTFILE '/var/lib/mysql-files/test.txt';

删除表中的数据:

DELETE FROM xiang.test;

从文本文件中恢复数据:

LOAD DATA INFILE '/var/lib/mysql-files/test.txt' INTO TABLE xiang.test;

举例2:

以固定格式导入数据文件

LOAD DATA INFILE '/var/lib/mysql-files/test.txt' INTO TABLE xiang.test FIELDS TERMINATED BY ',' ENCLOSED BY '\"';

7.6.2.2 使用mysqlimport

举例:

导出文件test.txt,字段之间使用逗号","间隔,字段值用双引号括起来:

SELECT * FROM xiang.test INTO OUTFILE '/var/lib/mysql-files/test.txt' FIELDSTERMINATED BY ',' ENCLOSED BY '\"';

删除表中的数据:

DELETE FROM xiang.test;

从文本文件中恢复数据:

mysqlimport -uroot -p xiang '/var/lib/mysql-files/test.txt' --fields-terminatedby=',' --fields-optionally-enclosed-by='\"

7.7 数据库迁移

7.7.1 概述

数据迁移(data migration)是指选择、准备、提取和转换数据,并将数据从一个计算机存储系统永久地传输到另一个计算机存储系统的过程。

此外, 验证迁移数据的完整性和退役原来旧的数据存储,也被认为是整个数据迁移过程的一部分。

数据库迁移的原因是多样的,包括服务器或存储设备更换、维护或升级,应用程序迁移,网站集成,灾难恢复和数据中心迁移。

根据不同的需求可能要采取不同的迁移方案,但总体来讲,MySQL 数据迁移方案大致可以分为物理迁移和逻辑迁移两类。通常以尽可能自动化的方式执行,从而将人力资源从繁琐的任务中解放出来。

7.7.2 迁移方案

物理迁移

物理迁移适用于大数据量下的整体迁移。使用物理迁移方案的优点是比较快速,但需要停机迁移并且要求 MySQL 版本及配置必须和原服务器相同,也可能引起未知问题。物理迁移包括拷贝数据文件和使用 XtraBackup 备份工具两种。不同服务器之间可以采用物理迁移,我们可以在新的服务器上安装好同版本的数据库软件,创建好相同目录,建议配置文件也要和原数据库相同,然后从原数据库方拷贝来数据文件及日志文件,配置好文件组权限,之后在新服务器这边使用mysqld命令启动数据库。

逻辑迁移

逻辑迁移适用范围更广,无论是部分迁移还是全量迁移 ,都可以使用逻辑迁移。逻辑迁移中使用最多的就是通过mysqldump等备份工具。

7.7.3 迁移注意点

1. 相同版本的数据库之间迁移注意点

指的是在主版本号相同的MySQL数据库之间进行数据库移动。

方式1: 因为迁移前后MySQL数据库的主版本号相同 ,所以可以通过复制数据库目录来实现数据库迁移,但是物理迁移方式只适用于MyISAM引擎的表。对于InnoDB表,不能用直接复制文件的方式备份数据库。

方式2: 最常见和最安全的方式是使用 mysqldump命令 导出数据,然后在目标数据库服务器中使用MySQL命令导入。

举例:

#host1的机器中备份所有数据库,并将数据库迁移到名为host2的机器上 mysqldump –h host1 –uroot –p –-all-databases | mysql –h host2 –uroot –p

在上述语句中,“|”符号表示管道,其作用是将mysqldump备份的文件给mysql命令;“--all-databases”表示要迁移所有的数据库。通过这种方式可以直接实现迁移。

2. 不同版本的数据库之间迁移注意点

例如,原来很多服务器使用5.7版本的MySQL数据库,在8.0版本推出来以后,改进了5.7版本的很多缺陷,因此需要把数据库升级到8.0版本.

旧版本与新版本的MySQL可能使用不同的默认字符集,例如有的旧版本中使用latin1作为默认字符集,而最新版本的MySQL默认字符集为utf8mb4。

如果数据库中有中文数据,那么迁移过程中需要对默认字符集进行修改 ,不然可能无法正常显示数据。

高版本的MySQL数据库通常都会兼容低版本 ,因此可以从低版本的MySQL数据库迁移到高版本的MySQL数据库。

3. 不同数据库之间迁移注意点

不同数据库之间迁移是指从其他类型的数据库迁移到MySQL数据库,或者从MySQL数据库迁移到其他类型的数据库。这种迁移没有普适的解决方法。

迁移之前,需要了解不同数据库的架构, 比较它们之间的差异。

不同数据库中定义相同类型的数据的关键字可能会不同 。例如,

  • MySQL中日期字段分为DATE和TIME两种,而ORACLE日期字段只有DATE;
  • SQLServer数据库中有ntext、Image等数据类型,MySQL数据库没有这些数据类型;
  • MySQL支持的ENUM和SET类型,这些SQL Server数据库不支持。

另外,数据库厂商并没有完全按照SQL标准来设计数据库系统,导致不同的数据库系统的SQL语句有差别。例如,微软的SQL Server软件使用的是T-SQL语句,T-SQL中包含了非标准的SQL语句,不能和MySQL的SQL语句兼容。

不同类型数据库之间的差异造成了互相迁移的困难 ,这些差异其实是商业公司故意造成的技术壁垒。但是不同类型的数据库之间的迁移并不是完全不可能 。例如,可以使用 MyODBC实现MySQL和SQL Server之间的迁移。MySQL官方提供的工具 MySQL Migration Toolkit也可以在不同数据之间进行数据迁移。MySQL迁移到Oracle时,需要使用mysqldump命令导出sql文件,然后, 手动更改sql文件中的CREATE语句。

7.7.4 迁移小结

7.8 误删库后如何处理

7.8.1 delete:误删行

经验之谈:

  1. 恢复数据比较安全的做法,是恢复出一个备份 ,或者找一个从库作为临时库 ,在这个临时库上执行这些操作,然后再将确认过的临时库的数据,恢复回主库。如果直接修改主库,可能导致对数据的二次破坏。
  2. 当然,针对预防误删数据的问题,建议如下:
    1. 把sql_safe_updates参数设置为on 。这样一来,如果我们忘记在delete或者update语句中写where条件,或者where条件里面没有包含索引字段的话,这条语句的执行就会报错。如果确定要把一个小表的数据全部删掉,在设置了sql_safe_updates=on情况下,可以在delete语句中加上where条件,比如where id>=0。
    2. 代码上线前,必须经过 SQL审计 。

7.8.2 truncate/drop:误删库\表

方案:

这种情况下,要想恢复数据,就需要使用全量备份 ,加增量日志 的方式了。这个方案要求线上有定期的全量备份,并且实时备份binlog。

在这两个条件都具备的情况下,假如有人中午12点误删了一个库,恢复数据的流程如下:

  1. 取最近一次全量备份 ,假设这个库是一天一备,上次备份是当天凌晨2点 ;
  2. 用备份恢复出一个 临时库 ;
  3. 从日志备份里面,取出凌晨2点之后的日志;
  4. 把这些日志,除了误删除数据的语句外,全部应用到临时库。

7.8.3 延迟复制备库

如果有非常核心的业务,不允许太长的恢复时间,可以考虑搭建延迟复制的备库。一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。

延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N 命令,可以指定这个备库持续保持跟主库有N秒的延迟 。比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。

7.8.4 预防误删库表的方法

  1. 账号分离 。这样做的目的是,避免写错命令。比如:
    1. 只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,可以通过开发管理系统得到支持。
    2. 即使是DBA团队成员,日常也都规定只使用只读账号 ,必要的时候才使用有更新权限的账号。
  2. 制定操作规范 。比如:
    1. 在删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
    2. 改表名的时候,要求给表名加固定的后缀(比如加 _to_be_deleted ),然后删除表的动作必须通过管理系统执行。并且,管理系统删除表的时候,只能删除固定后缀的表。

7.8.5 rm:误删mysql实例

对于一个有高可用机制的MySQL集群来说,不用担心rm删除数据 了。只是删掉了其中某一个节点的数据的话,HA系统就会开始工作,选出一个新的主库,从而保证整个集群的正常工作。我们要做的就是在这个节点上把数据恢复回来,再接入整个集群。

7.9 Mysql常用命令

7.9.1 mysql

该mysql不是指mysql服务,而是指mysql的客户端工具。语法 :

1. 连接选项

#参数 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器IP或域名
-P, --port=# 指定连接端口
#示例 :
mysql -h 127.0.0.1 -P 3306 -u root -p
mysql -h127.0.0.1 -P3306 -uroot -p密码

2. 执行选项

-e, --execute=name 执行SQL语句并退出

此选项可以在Mysql客户端执行SQL语句,而不用连接到MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便。

mysql -uroot -p db01 -e "select * from tb_book";

7.9.2 mysqladmin

mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。

可以通过 : mysqladmin --help 指令查看帮助文档

#示例 :
mysqladmin -uroot -p create 'test01';
mysqladmin -uroot -p drop 'test01';
mysqladmin -uroot -p version;

7.9.3 mysqlbinlog

由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog日志管理工具。

语法 :

mysqlbinlog [options] log-files1 log-files2 ...
#选项:
-d, --database=name : 指定数据库名称,只列出指定的数据库相关操作。
-o, --offset=# : 忽略掉日志中的前n行命令。
-r,--result-file=name : 将输出的文本格式日志输出到指定文件。
-s, --short-form : 显示简单格式, 省略掉一些信息。
--start-datatime=date1 --stop-datetime=date2 : 指定日期间隔内的所有日志。
--start-position=pos1 --stop-position=pos2 : 指定位置间隔内的所有日志。

7.9.4 mysqldump

mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句。语法 :

mysqldump [options] db_name [tables]
mysqldump [options] --database/-B db1 [db2 db3...]
mysqldump [options] --all-databases/-A

1.连接选项

#参数 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器IP或域名
-P, --port=# 指定连接端口

2.输出内容选项

#参数:
--add-drop-database 在每个数据库创建语句前加上 Drop database 语句
--add-drop-table 在每个表创建语句前加上 Drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table)
-n, --no-create-db 不包含数据库的创建语句
-t, --no-create-info 不包含数据表的创建语句
-d --no-data 不包含数据
-T, --tab=name 自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件,相当于select into outfile

#示例 :

mysqldump -uroot -p db01 tb_book --add-drop-database --add-drop-table > a
mysqldump -uroot -p -T /tmp test city

7.9.5 mysqlimport/source

mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件。语法:

mysqlimport [options] db_name textfile1 [textfile2...]

示例:

mysqlimport -uroot -p test /tmp/city.txt

如果需要导入sql文件,可以使用mysql中的source 指令 :

source /root/tb_book.sql

7.9.6 mysqlshow

mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。语法:

mysqlshow [options] [db_name [table_name [col_name]]]

参数:

--count 显示数据库及表的统计信息(数据库,表 均可以不指定)
-i 显示指定数据库或者指定表的状态信息

示例:

#查询每个数据库的表的数量及表中记录的数量
mysqlshow -uroot -p --count
mbp:~ xiang$ mysqlshow -uroot -p --count
Enter password: 
+--------------------+--------+--------------+
|     Databases      | Tables |  Total Rows  |
+--------------------+--------+--------------+
| mysql              | N/A    | N/A          |
| omc_rm             | N/A    | N/A          |
| performance_schema | N/A    | N/A          |
| REDIRECT           | N/A    | N/A          |
| sys                | N/A    | N/A          |
| test               | N/A    | N/A          |
| xiang              | N/A    | N/A          |
+--------------------+--------+--------------+

#查询test库中每个表中的字段书,及行数
mysqlshow -uroot -p xiang --count
mbp:~ xiang$ mysqlshow -uroot -p xiang  --count
Enter password: 
Database: xiang
+-------------+----------+------------+
|   Tables    | Columns  | Total Rows |
+-------------+----------+------------+
| emp1        |        4 |          2 |
| employee    |        5 |          0 |
| employee2   |        3 |          0 |
| event       |        3 |          3 |
| score       |        2 |          1 |
| test        |        3 |          1 |
| xiangMyISAM |        1 |          0 |
+-------------+----------+------------+
7 rows in set.

# 查询xiang库中test表的详细情况
mbp:~ xiang$ mysqlshow -uroot -p xiang test --count
Enter password: 
Database: xiang  Table: test  Rows: 1
+-------+------+-----------+------+-----+---------+-------------------+---------------------------------+---------+
| Field | Type | Collation | Null | Key | Default | Extra             | Privileges                      | Comment |
+-------+------+-----------+------+-----+---------+-------------------+---------------------------------+---------+
| a     | int  |           | YES  |     |         |                   | select,insert,update,references |         |
| b     | int  |           | YES  |     |         |                   | select,insert,update,references |         |
| c     | int  |           | YES  |     |         | VIRTUAL GENERATED | select,insert,update,references |         |
+-------+------+-----------+------+-----+---------+-------------------+---------------------------------+---------+

八、结束

  • 容易走的路,都是下坡路。当你觉得选择的路很艰难,很累,很难受的时候。说明你可能在成长,你在走上坡路。
  • 当你觉得选择的路很容易,很爽,很舒服的时候。说明你可能在逃避,你在走下坡路。
  • 顶尖高手,比的是慢、是笨、是扎实,是聪明人下笨功夫。
  • 时间是什么?时间是用来打造你的核心竞争壁垒的工具!

Guess you like

Origin blog.csdn.net/guaituo0129/article/details/130675141