MYSQL 最左前缀法则

什么是最左前缀法则呢?最左前缀法则是指复合索引当中要遵守的法则,先说结论:指查询从索引的最左列开始,并且不跳过索引中的列。也就是说查询的条件当中要包含索引最左侧的列,并且不能跳过索引当中的列。

如何理解呢?实际来个例子吧

create index idx_seller_name_sta_addr on tb_seller(name, status, address);
explain select * from tb_seller where name = '小米科技' and status = '1' and address ='北京市' //针对上面的索引,这样来查肯定是没有问题的

接下来再做一个测试 :

explain select * from tb_seller where name = '小米科技' //这样会走索引吗?结论是:也会

接下来进一步做测试:

explain select * from tb_seller where name = '小米科技' and status = '1'  //结论:也会走索引,可以看key_len区分索引的不同

接下来再做一下测试:

explain select * from tb_seller where status = '1' and address = '北京市' //这样走索引吗?不走,因为没从索引最左边的列开始

再做一轮测试:

explain select * from tb_seller where address = '北京市' //这样走索引吗?不走,因为也没从索引最左边的列开始

再做一轮测试:name顺序发生变化

explain select * from tb_seller where status = '1' and address = '北京市' and name = '小米科技' //走索引,顺序并不影响,只看查询条件有没有包含最左列

还有一种特殊情况:

explain select * from tb_seller where name = '小米科技' and address = '北京市' //走索引,只用到了name字段的索引,没用到address字段的索引

可以把这个想象成爬楼梯,name 第一层,status 第二层,address 第三层,不能跳过第一层,直接爬后面的。也不能爬完第一层后直接爬第三层。

mysql计算db size

SELECT sum(TABLE_ROWS) as rows, sum(DATA_LENGTH)/1024/1024 as data_size, sum(INDEX_LENGTH)/1024/1024 as index_size FROM  information_schema.`TABLES` WHERE TABLE_SCHEMA='库名'

db_size = data_size + index_size

//更多

//第一种情况:查询所有数据库的总大小,方法如下:
mysql> use information_schema;
mysql> select concat(round(sum(DATA_LENGTH/1024/1024),2),'MB') as data from TABLES;
+-----------+
| data      |
+-----------+
| 3052.76MB |
+-----------+
1 row in set (0.02 sec)

//统计一下所有库数据量 
每张表数据量=AVG_ROW_LENGTH*TABLE_ROWS+INDEX_LENGTH
SELECT
SUM(AVG_ROW_LENGTH*TABLE_ROWS+INDEX_LENGTH)/1024/1024 AS total_mb
FROM information_schema.TABLES 
统计每个库大小:
SELECT
table_schema,SUM(AVG_ROW_LENGTH*TABLE_ROWS+INDEX_LENGTH)/1024/1024 AS total_mb
FROM information_schema.TABLES group by table_schema;  


//第二种情况:查看指定数据库的大小,比如说:数据库test,方法如下:
mysql> use information_schema;
mysql> select concat(round(sum(DATA_LENGTH/1024/1024),2),'MB') as data from TABLES where table_schema='test';
+----------+
| data     |
+----------+
| 142.84MB |
+----------+
1 row in set (0.00 sec)

//1.查看所有数据库各容量大小
select
table_schema as '数据库',
sum(table_rows) as '记录数',
sum(truncate(data_length/1024/1024, 2)) as '数据容量(MB)',
sum(truncate(index_length/1024/1024, 2)) as '索引容量(MB)'
from information_schema.tables
group by table_schema
order by sum(data_length) desc, sum(index_length) desc;

//2.查看所有数据库各表容量大小
select
table_schema as '数据库',
table_name as '表名',
table_rows as '记录数',
truncate(data_length/1024/1024, 2) as '数据容量(MB)',
truncate(index_length/1024/1024, 2) as '索引容量(MB)'
from information_schema.tables
order by data_length desc, index_length desc;

//3.查看指定数据库容量大小
例:查看mysql库容量大小
select
table_schema as '数据库',
sum(table_rows) as '记录数',
sum(truncate(data_length/1024/1024, 2)) as '数据容量(MB)',
sum(truncate(index_length/1024/1024, 2)) as '索引容量(MB)'
from information_schema.tables
where table_schema='mysql'; 

//4.查看指定数据库各表容量大小
//例:查看mysql库各表容量大小
select
table_schema as '数据库',
table_name as '表名',
table_rows as '记录数',
truncate(data_length/1024/1024, 2) as '数据容量(MB)',
truncate(index_length/1024/1024, 2) as '索引容量(MB)'
from information_schema.tables
where table_schema='mysql'
order by data_length desc, index_length desc;

mysql processlist聚合分析

昨晚db告警,从慢日志和异常日志行为分析,表现都不太明显(并不像以往api请求变更历史那样有明显的IP和action或是用户信息,比较分散),虽然log初步分析是api流量有点大,但是最后都由于自己不够坚定而放弃了怀疑,说白了,还是自己技术不够到位,事后分析了下,慢日志和异常行为日志这两者如果不明显,但是db进程数(出现很多sleep进程)一直告警,是否可以通过processlist host出现的次数,来判断sql来源呢?如果host指向api,是不是就可以坐实了,那如何统计processlist host出现的次数?

show full processlist;并不支持类似like的过滤。于是想通过这个命令筛选得到某些服务器IP的连接不可取。但并不是没有好的办法,因为show full processlist;命令本身就是查询表PROCESSLIST里的内容,表PROCESSLIST在数据库information_schema中,我们在执行show processlist时实际在mysql中就是执行查询information_schema数据库中的processlist表查询。这就好办了,因为字段HOST中的值多是ip:port,这种IP加端口结构(当然我上面有的是localhost,这个不用考虑,大网站都会是多个WEB连接MYSQL,基本都是IP端口),所以上面使用字符串以逗号分隔,从而截取IP地址出来。所以如果想根据HOST中的连接IP地址来进行分组统计,可以使用SQL:

select server,count(server) as servernum from (select substring_index(host,':' ,1) as server from processlist ) as rsa group by server order by servernum desc;

substring_index(字段名, ‘:’, 1)的作用:截取第1个 ‘:’ 之前的所有字符。非常有用。

其实除此之外,还可以通过其他手段坐实问题
1.API容器当中php slow log (/xxxx/php/slow.log)
2.API容器当中php error.log(/xxxx/php/error.log)

Mysql 获取每小时及每天的数据量

统计每小时

SELECT HOUR(trigger_time) as Hour,count(*) as Count 
    FROM xxl_job_qrtz_trigger_log 
   WHERE
	trigger_time BETWEEN '2018-02-05 01:18:36'
AND '2018-02-05 17:18:36'
    GROUP BY HOUR(trigger_time) ORDER BY Hour(trigger_time);

统计每天

SELECT
	DATE_FORMAT(trigger_time, '%Y-%m-%d') triggerDay,
	COUNT(id) triggerCount
FROM
	`job_qrtz_trigger_log`
WHERE
	trigger_time BETWEEN '2018-02-02 09:18:36'
AND '2018-03-05 23:18:36'
GROUP BY triggerDay
ORDER BY trigger_time;

DB DDL和DML

今天在用yearning 平台提交SQL时,突然看到一个熟悉的缩写,但是一时想不起来,哪个是哪个,记录一下:

DDL (Data Definition Language 数据定义语言)

  • create table
  • alter table
  • drop table
  • truncate table
  • create index
  • drop index

当执行DDL语句时,在每一条语句前后,oracle 都将提交当前的事务。如果用户使用insert 命令将记录插入到数据库后,执行一条DDL语句(如create table),此时来自insert 命令的数据将被提交到数据库。当DDL语句执行完成时,DDL语句会被自动提交,不能回滚

DML(Data Manipulation Language 数据操作语言)

  • insert 插入db 记录
  • update 修改db 记录
  • delete 删除db 记录

当执行DML命令如果没有提交,将不会被其他会话看到,除非在DML命令之后执行DDL命令或DCL命令,或用户退出会话,或终止实例,此时系统会自动发出commit 命令,使未提交的DML命令提交

Mysql Specified key was too long 解决方案

昨晚在执行migration db变更时,报了一个错误:

CREATE TABLE `xxxx_configs` (
        `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        `rules` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '例如:gantts_setting_var1_var2,var1=workspace_id,var2=user_id,具体规
则在代码中设置',
        `abc` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'xxx',
        `def` int(11) NOT NULL DEFAULT 0 COMMENT,
        `ghi` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '0(未启用), 1(启用)',
        PRIMARY KEY (`id`),
        UNIQUE INDEX `rules` (`rules`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;;

Reason: Specified key was too long; max key length is 767 bytes

分析:

对于报错信息“: Specified key was too long; max key length is 767 bytes”,其实意思就是“索引字段长度太长,超过了767bytes”。

mysql的varchar主键只支持不超过767个字节或者768/2=384个双字节 或者767/3=255个三字节的字段 而GBK是双字节的,UTF8是三字节的。

那么问题来了,为什么内网版库执行正常,但是SaaS版执行异常呢?

(1) 字符集设置不同?

第一个想法是查看两个库的字符集设置是不是不一样,果然内网库的字符集character_set_database=lantin1,而saas库的字符集character_set_database= utf8。这时候,我很天真的以为可能是因为建表的时候没有指定字符集,所以在导入测试库的时候默认使用utf-8字符集,导致Specified key was too long。所以就更改了saas库中默认的字符集,但是导入数据的时候依然还是报同样的错误。而且我在建表的语句中发现了,其实原来就已经指定了字符集,都是UTF8(CHARSET=UTF8),所以字符集设置不同并不是问题的症结所在。下面是用show variable_name like ‘%character_set%’的展示结果:

内网库
云端库

(2)存储引擎惹的祸?

我查了一下,内网的SQL语句和SaaS的SQL语句是一致的,同一条语句指定的engine是一样的,所以也并非是engine的锅

(3)Mysql版本的锅?

查了一圈资料,在MySQL 5.6及之前的版本,使用InnoDB作引擎的表的索引大小要小于767B,对于MyISAM的限制则是1000B。在MySQL5.7之后此限制扩展到了3027B

内网是5.7版本的SQL, SaaS版是 基于MariaDB 10.1.9设计(兼容Mysql 5.6) ,应该是Mysql 5.5的版本(Server version: 5.5.5-10.1.44-MariaDB)

(4)解决方法?

减少索引列的长度 将varchar(512) 改成 varchar(255)

参考文章:
https://cloud.tencent.com/developer/article/1005696
https://blog.csdn.net/weixin_36210698/article/details/81568230

mysql不带%的like 与等号之间的区别

SELECT * FROM pet WHERE name LIKE 'Spot'; 
SELECT * FROM pet WHERE name = 'Spot'; 

上面这两个sql语句有什么区别呢? 今天在值班时遇到一个人名联想的问题,当用户输入xyz,只出现一个人名,而如果xyza, 则出现了好几个候选人名,好奇了下搜索结果,查了下代码,代码先用的全匹配,再用了模糊匹配,如果输入的内容在人名表里存在,则精确出现该人名,如果内容不能全匹配,则走到模糊匹配上,全匹配的时候用的是like 不带百分号,一时好奇,不带百分号的like 和等号的区别,于是有了这篇文章,实际上,没有通配符的LIKE在功能上等同于=。但是,他们又有点不一样! 这就要说到他们的工作方式,在网上搜了一圈,发现一篇比较好的解释:

下面内容出自:https://stackoverflow.com/questions/543580/equals-vs-like

LIKE and = are different operators. Most answers here focus on the wildcard support, which is not the only difference between these operators!

= is a comparison operator that operates on numbers and strings. When comparing strings, the comparison operator compares whole strings.

LIKE is a string operator that compares character by character.

To complicate matters, both operators use a collation which can have important effects on the result of the comparison.

How Does = Work?
The SQL Standard § 8.2 describes how = compares strings:

The comparison of two character strings is determined as follows:

a) If the length in characters of X is not equal to the length in characters of Y, then the shorter string is effectively replaced, for the purposes of comparison, with a copy of itself that has been extended to the length of the longer string by concatenation on the right of one or more pad characters, where the pad character is chosen based on CS. If CS has the NO PAD attribute, then the pad character is an implementation-dependent character different from any character in the character set of X and Y that collates less than any string under CS. Otherwise, the pad character is a .

b) The result of the comparison of X and Y is given by the collating sequence CS.

c) Depending on the collating sequence, two strings may compare as equal even if they are of different lengths or contain different sequences of characters. When the operations MAX, MIN, DISTINCT, references to a grouping column, and the UNION, EXCEPT, and INTERSECT operators refer to character strings, the specific value selected by these operations from a set of such equal values is implementation-dependent.

(Emphasis added.)

What does this mean? It means that when comparing strings, the = operator is just a thin wrapper around the current collation. A collation is a library that has various rules for comparing strings. Here’s an example of a binary collation from MySQL:

static int my_strnncoll_binary(const CHARSET_INFO *cs attribute((unused)),
                                const uchar *s, size_t slen,
                                const uchar *t, size_t tlen,
                                my_bool t_is_prefix)
 {
   size_t len= MY_MIN(slen,tlen);
   int cmp= memcmp(s,t,len);
   return cmp ? cmp : (int)((t_is_prefix ? len : slen) - tlen);
 }

This particular collation happens to compare byte-by-byte (which is why it’s called “binary” — it doesn’t give any special meaning to strings). Other collations may provide more advanced comparisons.

For example, here is a UTF-8 collation that supports case-insensitive comparisons. The code is too long to paste here, but go to that link and read the body of my_strnncollsp_utf8mb4(). This collation can process multiple bytes at a time and it can apply various transforms (such as case insensitive comparison). The = operator is completely abstracted from the vagaries of the collation.

How Does LIKE Work?
The SQL Standard § 8.5 describes how LIKE compares strings:

M LIKE P

is true if there exists a partitioning of M into substrings such that:

i) A substring of M is a sequence of 0 or more contiguous s of M and each of M is part of exactly one substring.

ii) If the i-th substring specifier of P is an arbitrary character specifier, the i-th substring of M is any single .

iii) If the i-th substring specifier of P is an arbitrary string specifier, then the i-th substring of M is any sequence of 0 or more s.

iv) If the i-th substring specifier of P is neither an arbitrary character specifier nor an arbitrary string specifier, then the i-th substring of M is equal to that substring specifier according to the collating sequence of the , without the appending of characters to M, and has the same length as that substring specifier.

v) The number of substrings of M is equal to the number of substring specifiers of P.

(Emphasis added.)

This is pretty wordy, so let’s break it down. Items ii and iii refer to the wildcards _ and %, respectively. If P does not contain any wildcards, then only item iv applies. This is the case of interest posed by the OP.

In this case, it compares each “substring” (individual characters) in M against each substring in P using the current collation.

结论:

The bottom line is that when comparing strings, = compares the entire string while LIKE compares one character at a time. Both comparisons use the current collation. This difference leads to different results in some cases, as evidenced in the first example in this post.

详解Mysql 事务

学习一个东西,还是先从其概念开始,首先先看看什么是事务?

数据库事务是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。

从这个概念看,事务就是一组SQL,而且这组SQL要么全部执行,要么全部不执行。

那么第二个常见的问题,事务有哪些特性(点)呢?这是一个很常见的考题。

事务的四大特性:A(Atomicity)C(Consistency)I(Isolation)D(Durability),如何理解这四个特性呢?

1.原子性(Atomicity):事务开始后的所有操作,要么全部做完,要么全部不做,不可能做一半的情况,如果事务执行过程中出错,会回滚到事务开始前的状态,也就是事务是一个不可分割的整体。

2.一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,A扣了钱,B就一定会收到,不存在A这边减了,B那边没有加的情况。

3.隔离性(Isolation):同时发生的事务(并发事务)不应该导致数据库处于不一致的状态中。系统中每个事务都应该像唯一事务一样执行。任何事务都不应该影响其他事务的存在。假如A向B转账的同时,A完全转移所有的钱,两个事务应该独立进行,在进行转账前要确认好余额。关于隔离性,后面会详细介绍。

4.持久性(Durability)事务完成后,事务对数据库的所有更新将被保存到数据库(磁盘),不能回滚,也可以理解为:无论db或系统是否故障,数据都会永久保存在磁盘上,不会丢。

那Mysql是否支持事务,Mysql有多种存储引擎,包括MyISAM, Innodb, Memory, Merge等。这其中,Innodb和BDB是支持事务的,MyISAM不支持事务。

Mysql如何执行事务呢?

1.启动事务

mysql> start transaction;
或者
mysql> begin;

2.执行你希望在事务中运行的sql语句

3.执行commit语句,完成事务并提交数据

示例,假设有一张表student:

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  28 |
|  3 | lisi     |  28 |
+----+----------+-----+

我们来执行一个事务,将zhangsan的年龄改成22,具体语句如下:

#会话1
MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> 
MySQL [test]> 
MySQL [test]> 
MySQL [test]> 
MySQL [test]> update student set age = 22 where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

此时我们在会话1中没有执行commit,然后我们在另一个会话窗口2查询下id=2的学生记录,年龄仍然是28(尽管在当前事务会话(会话1)中能查到最新数据22),因为前一个会话并没有commit(Mysql默认的事务隔离级别是可重复读),所以我们读的仍然是事务执行完之前的数据。

#会话2
MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  28 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

当我将会话窗口1执行commit后,此时会话2的查询结果如下:

#会话1
MySQL [test]> commit;
Query OK, 0 rows affected (0.00 sec)
#会话2
MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

以上就是一个事务的执行过程。如果我们想中止这个事务,不提交的话,可以使用rollback:

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> update student set age = 32 where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> select * from student where id = 1;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | clark |  32 |
+----+-------+-----+
1 row in set (0.01 sec)

MySQL [test]> rollback;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> select * from student where id = 1;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | clark |  30 |
+----+-------+-----+
1 row in set (0.00 sec)

聊聊autocommit

默认情况下,autocommit 的状态是ON,这意味着所有单独的语句一旦被执行就会被提交,除非该语句在begin….commit语句块中。查看autocommit值:

MySQL [test]> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

如果autocommit的状态为off,则需要明确发出commit语句来提交事务。 要禁用autocommit,可以用下面命令:

MySQL [test]> set autocommit = 0;

PS: DDL语句,如create, alter, drop语句,都是无法rollback的。

聊完事务的概念及事务的用法之后,关于事务,还有一个很常见的问题:事务的隔离级别,这也是个常规考题。

事务的隔离级别,描述的是两个或多个事务同时发生时,一个事务与其他事务在资源或数据修改方面的隔离程度。事务的隔离级别有4个,分别是读未提交(read uncommitted),读已提交(read committed),可重复读(repeatable read),序列化(serializable)。

前面我们提过 MYSQL默认的事务隔离级别是:可重复读(repeatable read),如何修改隔离级别呢?可以使用set @@transaction_isolation=’xxx’;来修改事务的隔离级别。

那接下来我来分别详细说说四个事务隔离级别:

1.读未提交(read uncommitted):顾名思义,事务可以读取另一个未提交的事务写入的数据,读未提交也被称为脏读(因为它会导致脏读)。

我们举个例子,

假如老板给你发工资,转的金额原本应该是2w,但是不小心手抖输错了数字,输成了2.5w,该钱已打到你的账户,但是事务还没有提交,但是此时,你去查看自己的账户,发现多了5k块,窃喜以为涨工资了,老板及时发现不对,马上回滚了事务,将数字改成了2w,再commit,你就蒙圈了,刚刚看了还是2.5w的,这就出现了脏读

具体查询举例:

#会话1--会话1没有commit
MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.00 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> update student set age = 29 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
#会话2
MySQL [test]> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> 
MySQL [test]> 
MySQL [test]> select @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  29 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

在会话2中能看到会话1未提交的事务,这种情况就是读未提交。读未提交会出现脏读(一旦会话1 事务被rollback)

2.读已提交(read committed):当前事务只能读取另一个事务提交的数据,这被称为不可重复读(non-repeatable read),因为它会导致不可重复读。

具体举例:

#会话1
MySQL [test]> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  33 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> update student set age = 25 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> commit;
Query OK, 0 rows affected (0.01 sec)
#会话2
MySQL [test]> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> select * from student;  //会话(事务)1--commit前 
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  33 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> select * from student; //会话(事务)1--commit后
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

可以注意到,在同一个事务会话2中,相同的select语句获取不同的结果,一次age等于30,一次age等于29。所以出现不可重复读

3.可重复读(repeatable read)

一个事务通过第一条语句只能看到相同的数据,即使另一个事务已提交数据。在同一个事务中,读取通过第一次读取建立快照是一致的。一个例外,一个事务可以读取在同一个事务中更改的数据。

当事务开始并执行第一次读取数据时,将创建读取视图并保持打开状态,直到事务结束。为了在事务结束之前提供相同的结果集,InnoDB使用行版本控制和UNDO信息。假设事务1选择了几行,另一个事务删除了这些行并提交了数据。如果事务1处于打开状态,它应该能够看到自己在开始时选择的行。已被删除的行保留在 UNDO 日志空间中以履行事务1。一旦事务1操作完成,那些行便被标记为从UNDO日志中删除。这称为多版本并发控制(MVCC)。

具体举例:

#会话1
MySQL [test]> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> 
MySQL [test]> 
MySQL [test]> 
MySQL [test]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> update student set age = 27 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> commit;
Query OK, 0 rows affected (0.00 sec)
#会话2
MySQL [test]> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> select * from student; //会话(事务)1 commit 前
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> select * from student; //会话(事务)1 commit 后
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

可重复读保证了在一个事务中,无论怎么查,结果都是一致,不受其他事务是否commit影响。从上例可以看出,事务1提交前后,事务2的查询结果都不变。

这就是我们前面说的:当事务开始并执行第一次读取数据时,将创建读取视图并保持打开状态,直到事务结束。为了在事务结束之前提供相同的结果集。

从上面结果看,可重复读会产生幻读。那上面看下来,脏读,不可重复读,幻读有点像。那区别在哪呢?

脏读是指一个事务取到了其他事务没有提交的数据,
不可重复读是指一个事务内多次根据同一个查询条件查询出来的同一行记录的值不一样
幻读是指一个事务内多次根据同个条件查出来的记录行数不一样

大家弄懂了几个的区别吗?如果没懂,可以细细品上述加粗字体

由于可重复读会产生幻读,于是就有第四个事务隔离级别:序列/串行化(serializable)

通过把选定的所有行锁起来,序列化可以提供最高级别的隔离。此级别与REPEATABLE READ类似,但如果禁用autocommit,则InnoDB会将所有普通SELECT语句隐式转换为SELECT…LOCKIN SHARE MODE;如果启用autocommit,则SELECT就是它自己的事务,并且序列化会等待被锁的行,总是读取最新提交的数据。

串行化,顾名思义,是将所有读写操作完全串行。
串行化是所有隔离级别中最高的
每次读都需要获得表级共享锁,读写相互都会阻塞
串行化对资源的开销大,对并发支持不好,只在,某些场景下使用。

具体例子就不细列了,可以自己google

那么上面讲了这么多,现在来总结一下,下面是各个隔离级别的对照表:

隔离级别脏读不可重复读幻读
Read Uncommited可能可能可能
Read Commited不可能可能可能
Repeatable Read不可能不可能可能
Serializable不可能不可能不可能

线上数据库变更时如何保证业务正常

今天评审时,被评委问了一个问题,大致意思是问,生产环境你有这么多同构的数据库,你是怎么完成数据变更操作的?

其实这个问题考查两点:一个是你变更的手段/方案,另一个是数据库变更和新旧代码的适应问题

介于当时没有回答好,事后复盘总结梳理一下这个问题的答案。

首先说一下方案:完成几w个同构的数据库变更是怎样达成的?可以用migration数据库变更框架来完成,这个框架的大致思路,将数据变更的脚本(含有版本号信息),以多进程的方式重放到每个数据库当中来完成数据库的变更,并且重放过的变更记录下版本号,以确保后续的数据库变更不会再重复运行。

紧接着下一个问题(变更数据库时如何适应新旧代码,不影响线上服务),你的数据库变更如何应对线上的代码呢?当时我的回答并不好,回答的条理性和逻辑性不够,也许实际场景当中,我只遇到了加字段和删字段的场景,但是很少遇到将一个字段A直接改成字段B的场景(alter常见的是改字段类型,而非字段名),加上没有深入的思考过这个问题,所以卡壳了。那现在我重新梳理下这个问题的回答:

首先我们的数据库变更是会分为预发布前,预发布后和正式发布后的。之所以这样回答,说明我有考虑过,一个字段的变更,可能存在无法应对生产环境的旧代码的问题。有些数据库变更能在代码发布前操作(也就是正式发布之前操作,这种变更生产环境的旧代码是能适应的),而有些一定要上了新代码之后才能做变更,也就是正式发布之后跑(这种变更是旧代码是无法适应,比如你将字段A改成B,或是删除字段A, 旧代码都有可能无法适应)

但实际回答可以不用那么复杂和啰嗦,上面的回答只是先解释我有考虑过这个问题,而实际数据库变更,无非就三种情况,增、删、改。

下面来逐一分析下各种变更的实际应该如何操作:

1.增加字段

这种通常给默认值,旧代码就能适应,这种可以先变更数据库,再发代码

2.删除字段

应该先发代码,再执行变更,原因是如果先删字段,可能生产环境的旧代码有用到这个字段,而先变更会导致把字段删了,而代码有调用。

但是如果旧字段没有默认值呢?要先给旧字段添加默认值,否则新代码已经不需要这个字段,但是表的旧字段没删,但是表的旧字段又有必填,且没有默认值,一个保存操作,就gg了,新代码保存时会丢数据

3.变更字段(将字段A改成字段B)

方案1

1. 先添加一个字段B,并且将字段A的值拷贝到B中,并且建立触发器,将后续A的变更都同步到B,将B的变更,也同步到A,并且给字段B设定默认值

2. 发代码(这样发布期间,新旧代码共存时,旧代码能读到A,新代码能读到B,那此时如果旧代码对A的变更如何反馈到B呢?用步骤a的触发器将旧代码对A做的变更反馈到B上,同步也将新代码的变更反馈到旧的A字段上以适应旧代码),这一步最好将旧字段A也加上默认值以适应新代码保存逻辑

3. 最后再执行删除A的数据库变更

方案2:

1.先添加字段B

2.发新代码,保证双写(增改都要双写),但是前提:读,先保证新代码读旧字段A

3.写对账脚本,用脚本对账A, B两个字段,以保证两个字段的一致性

4.再改代码让新代码读新字段B再发代码,此时新旧业务上都能正常。

5.再改代码,取消双写只写新,发代码

6.删除旧字段A

整理导图如下: