느린 로그 문제로 인한 MySQL semi-consistent 읽기의 애플리케이션 시나리오

느린 로그 문제를 통해 저자는 MySQL semi-consistent 읽기의 개념과 실제 적용 시나리오를 소개합니다.

저자: 공탕제

Acson DBA 팀원으로 주로 MySQL 기술 지원을 담당하고 있으며 MySQL, PG 및 국내 데이터베이스에 능합니다.

이 기사의 출처: 원본 기여

  • Aikesheng 오픈 소스 커뮤니티에서 제작한 원본 콘텐츠는 승인 없이 사용할 수 없습니다. 편집자에게 연락하여 재인쇄를 위해 출처를 명시하세요.

배경

특정 시스템은 업데이트 작업이 매우 느리고 느린 로그가 많고 그 중 잠금 시간이 높은 비율을 차지한다는 것을 발견했습니다.MySQL 버전은 5.7.25이고 격리 수준은 RR입니다.

분석하다

UPDATE명령문의 테이블 구조와 실행 계획을 봅니다 .

mysql> show create table test;

+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| test | CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2621401 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> explain update test set name ='test' where name='a';
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| 1  | UPDATE      | test  | NULL       | index | NULL   | PRIMARY | 4 | NULL | 2355988 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
1 row in set (0.00 sec)

Execution Plan을 통해 SQL은 Primary Key에 대한 Full Index Scan으로 해당 컬럼에 nameIndex가 생성되지 않았으며, 여러 Transaction이 동시에 실행될 경우 Blocking이 관찰된다.

트랜잭션 1 트랜잭션 2
mysql> 시작;
쿼리 확인, 영향을 받는 행 0개(0.00초)
mysql> 업데이트 테스트 세트 이름 ='test' where name='a';
쿼리 확인, 영향을 받는 262144개 행(4.67초)
일치하는 행 수: 262144 변경됨: 262144 경고: 0
mysql> 시작;
쿼리 확인, 영향을 받는 행 0개(0.00초)
mysql> 업데이트 테스트 세트 이름 ='test1' where name='b';

name열에 중복 값이 ​​많지 않은 경우 name열에 인덱스를 추가하여 문제를 해결할 수 있습니다. InnoDB의 행 잠금 메커니즘은 인덱스 열을 기반으로 구현되기 때문에 명령문 이 열의 인덱스를 UPDATE사용할 수 있으면 차단이 발생하지 않아 비즈니스가 정지됩니다.name

그러나 name열 값의 변별력이 매우 낮은 경우 SQL은 name다음 예와 같이 열 인덱스를 실행하지 않습니다.

인덱스를 먼저 추가

mysql> alter table test add index tt(name);
Query OK, 0 rows affected (2.74 sec)
Records: 0 Duplicates: 0 Warnings: 0

그런 다음 실행 계획을 확인하고 사용할 수 있는 인덱스가 있는지 확인 tt하지만 실제 상황은 여전히 ​​기본 키 전체 인덱스 스캔입니다.

mysql> explain update test set name ='test' where name='a';
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| 1 | UPDATE | test | NULL | index | tt | PRIMARY | 4 | NULL | 2355988 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
1 row in set (0.00 sec)

MySQL의 옵티마이저는 비용을 기준으로 평가하기 때문에 optimizer trace.

mysql> show variables like 'optimizer_trace';
+-----------------+--------------------------+
| Variable_name | Value |
+-----------------+--------------------------+
| optimizer_trace | enabled=off,one_line=off |
+-----------------+--------------------------+
1 row in set (0.01 sec)

enabled=off이 기능이 기본적으로 비활성화되어 있음을 나타내는 값을 볼 수 있습니다 .

이 기능을 활성화하려면 먼저 enabled의 값을 로 변경 해야 합니다 on.

mysql> set optimizer_trace="enabled=on";
Query OK, 0 rows affected (0.00 sec)

그런 다음 SQL을 실행하여 자세한 정보를 확인합니다. 여기서는 주로 PREPARE 단계에서 비용 계산에 중점을 둡니다.

mysql> update test set name ='test' where name='a';
Query OK, 262144 rows affected (5.97 sec)
Rows matched: 262144 Changed: 262144 Warnings: 0

mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE\G 

자세한 결과는 다음과 같습니다.

mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE\G
*************************** 1. row ***************************
QUERY: update test set name ='test' where name='a'
TRACE: {
"steps": [
{
"substitute_generated_columns": {
}
},
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`test`.`name` = 'a')",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "multiple equal('a', `test`.`name`)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "multiple equal('a', `test`.`name`)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "multiple equal('a', `test`.`name`)"
}
]
}
},
{
"table": "`test`",
"range_analysis": {
"table_scan": {
"rows": 2355988,
"cost": 475206
},
"potential_range_indexes": [
{
"index": "PRIMARY",
"usable": true,
"key_parts": [
"id"
]
},
{
"index": "tt",
"usable": true,
"key_parts": [
"name",
"id"
]
}
],
"setup_range_conditions": [
],
"group_index_range": {
"chosen": false,
"cause": "no_join"
},
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "tt",
"ranges": [
"0x0100610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 <= name <= 0x0100610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": true,
"using_mrr": false,
"index_only": false,
"rows": 553720,
"cost": 664465,
"chosen": false,
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
}
}
}
]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)

전체 테이블 스캔을 수행하는 비용은 475206 이고 인덱스를 실행하는 비용 tt664465 이므로 MySQL은 전체 테이블 스캔을 선택합니다 .

그렇다면 이런 경우 어떻게 대처해야 할까요?

InnoDB 격리 수준이 RR이면 데이터베이스 수준에서 좋은 방법이 없으며 응용 프로그램 측에서 수정하는 것이 좋습니다.

데이터베이스 격리 수준을 변경할 수 있는 경우 RC로 변경하여 차단 문제를 해결할 수 있습니다. RC 모드에서 반일관적인 읽기가 지원되기 때문입니다.

준일관 읽기란 무엇입니까?

간단히 말해서 행을 잠글 필요가 있을 때 행을 잠글 필요가 있는지 여부를 판단하는 데 한 단계 더 걸립니다. 예를 들어 전체 테이블을 스캔하고 업데이트할 때 일치하는 행만 업데이트하면 됩니다.semi WHERE-consistent 읽기가 없으면 모든 데이터가 잠기지만 semi-consistent 읽기는 다음 여부를 판단합니다. WHERE조건이 만족되지 않으면 만족하지 않으면 잠금이 추가되지 않습니다(미리 잠금이 해제됨).

그런 다음 차별성이 낮은 필드의 경우 준일관 읽기 기능을 사용하여 최적화할 수 있으므로 서로 다른 값을 업데이트해도 서로 기다리지 않아 비즈니스가 정지됩니다.

트랜잭션 1 트랜잭션 2
mysql> 시작;
쿼리 확인, 영향을 받는 행 0개(0.00초)
mysql> 업데이트 테스트 세트 이름 ='test' where name='a';
쿼리 확인, 262144개의 행이 영향을 받음(9.30초)
일치하는 행: 262144 변경됨: 262144 경고: 0
mysql> 시작;
쿼리 확인, 영향을 받는 행 0개(0.00초)
mysql> 업데이트 테스트 세트 이름 ='test1' where name='b';
쿼리 확인, 영향을 받는 262144개 행(8.46초)
일치하는 행 수: 262144 변경됨: 262144 경고: 0

결론적으로

  1. 행 잠금 메커니즘은 인덱스 열을 기반으로 구현되며 인덱스를 사용하지 않으면 전체 테이블 스캔이 수행됩니다.
  2. 준일관 읽기는 RC 격리 수준을 기반으로 한 최적화로, 잠금 충돌과 잠금 대기를 줄이고 동시성을 향상시킬 수 있습니다.

SQLE 정보

Akson 오픈 소스 커뮤니티의 SQLE는 데이터베이스 사용자 및 관리자를 위한 SQL 감사 도구로 다중 시나리오 감사를 지원하고 표준화된 온라인 프로세스를 지원하며 기본적으로 MySQL 감사를 지원하고 확장 가능한 데이터베이스 유형을 가지고 있습니다.

SQLE 가져오기

유형 주소
저장소 https://github.com/actiontech/sqle
문서 https://actiontech.github.io/sqle-docs/
릴리스 뉴스 https://github.com/actiontech/sqle/releases
데이터 감사 플러그인 개발 문서 https://actiontech.github.io/sqle-docs-cn/3.modules/3.7_auditplugin/auditplugin_development.html

추천

출처blog.csdn.net/ActionTech/article/details/131440819