【数据库】1、MySQL、事务、MVCC、LBCC

数据库相关文章:

  • 数据库①:基础、事务、锁:https://blog.csdn.net/hancoder/article/details/105773038
  • 数据库②:索引、调优、explain(尚硅谷笔记)https://blog.csdn.net/hancoder/article/details/105773095
  • 数据库③:存储结构、页、聚簇索引 https://blog.csdn.net/hancoder/article/details/105773153

三范式

第一范式(1NF):指表的列不可再分,数据库中表的每一列都是不可分割的基本数据项,同一列中不能有多个值;

第二范式(2NF):在 1NF 的基础上,还包含两部分的内容:一是表必须有一个主键;二是表中非主键列必须完全依赖于主键,不能只依赖于主键的一部分;

例如:选课关系(学号,课程名称,成绩,学分),组合关键字(学号,课程名称)作为主键,其不满足2NF,

第三范式(3NF):在 2NF 的基础上,消除非主键列对主键的传递依赖,非主键列必须直接依赖于主键。

例如:学生表(学号,姓名,学院编号,学院名称),学号是主键,姓名、学院编号、学院名称都完全依赖于学号,
         满足2NF,但不满足3NF,因为学院名称直接依赖的是学院编号 ,它是通过传递才依赖于主键.

BC范式(BCNF):在 3NF 的基础上,消除主属性对于码部分的传递依赖

约束:

  • NOT NULL:非空,用于保证该字段的值不能为空。

    • -- 此时表示将第一个字段的值设为null, 取决于该字段是否允许为null
      insert into tab values (null, 'val'); 
      
  • DEFAULT:默认值,用于保证该字段有默认值

    • -- 创建:
      create table tab ( add_time timestamp default current_timestamp );
      -- 表示将当前时间的时间戳设为默认值。选项:current_date, current_time
      
      -- 此时表示强制使用默认值。
      insert into tab values (default, 'val');    
      
  • PRIMARY KEY:主键,用于保证该字段的值具有【唯一性】,一个表中有一个主键

    • -- 声明方式:1 跟在声明的列后 2 在列的最后`
      create table tab (
          id int,
          stu varchar(10),
          primary key (id));
      -- 主键字段非空
      -- 主键可以由多个字段组成`
      create table tab ( id int, stu varchar(10), age int, 
                        primary key (stu, age));
      
  • UNIQUE:唯一,用于保证该字段的值具有唯一性,可以为空(对比主键)

  • AUTO_INCREMENT: 自增约束。自动增长必须为索引(主键或unique)

    • 默认为1开始自动增长。可以通过表属性 auto_increment = x进行设置,或 alter table tbl auto_increment = x;
  • CHECK:检查约束【mysql中不支持】

  • FOREIGN KEY:外键,用于限制主从表的关系,用于保证该字段的值必须来自于主表的关联列的值

    • 在从表添加外键约束,用于【引用主表】中某列的值
    • 比如学生表的专业编号,员工表的部门编号,员工表的工种编号
  • COMMENT:注释。

    • create table tab ( id int  comment '注释内容' );
      
7. FOREIGN KEY 外键约束
    用于限制主表与从表数据完整性。
    alter table t1 add constraint `t1_t2_fk` foreign key (t1_id) references t2(id);
        -- 将表t1的t1_id外键关联到表t2的id字段。
        -- 每个外键都有一个名字,可以通过 constraint 指定
    存在外键的表,称之为从表(子表),外键指向的表,称之为主表(父表)。
    作用:保持数据一致性,完整性,主要目的是控制存储在外键表(从表)中的数据。
    MySQL中,可以对InnoDB引擎使用外键约束:
    语法:
    foreign key (外键字段) references 主表名 (关联字段) [主表记录删除时的动作] [主表记录更新时的动作]
    此时需要检测一个从表的外键需要约束为主表的已存在的值。外键在没有关联的情况下,可以设置为null.前提是该外键列,没有not null。
    可以不指定主表记录更改或更新时的动作,那么此时主表的操作被拒绝。
    如果指定了 on update 或 on delete:在删除或更新时,有如下几个操作可以选择:
    1. cascade,级联操作。主表数据被更新(主键值更新),从表也被更新(外键值更新)。主表记录被删除,从表相关记录也被删除。
    2. set null,设置为null。主表数据被更新(主键值更新),从表的外键被设置为null。主表记录被删除,从表相关记录外键被设置成null。但注意,要求该外键列,没有not null属性约束。
    3. restrict,拒绝父表删除和更新。
    注意,外键只被InnoDB存储引擎所支持。其他引擎是不支持的。
# 外键例子---列级约束例子------
CREATE TABLE stuiInfo(
	id INT PRIMARY KEY,#主键
	stuName VARCHAR(20) NOT NULL UNIQUE,#非空
	gender CHAR(1) CHECK(gender='男' OR gender ='女'),#检查
	seat INT UNIQUE,#唯一
	age INT DEFAULT  18,#默认约束
	majorId INT REFERENCES major(id)#外键-!!!
);

CREATE TABLE major(
	id INT PRIMARY KEY,
	majorName VARCHAR(20)
);

#查看stuinfo中的所有索引,包括主键、外键、唯一
SHOW INDEX FROM stuinfo;
  • 列级约束:六大约束语法上都支持,但外键约束没有效果(外键约束写了也白写)
  • 表级约束:除了非空、默认,其他的都支持
列级约束的例子即上个程序

表级约束语法:在各个字段的最下面
 【constraint 约束名】 约束类型(字段名)
 
# 表级约束例子--------
DROP TABLE IF EXISTS stuinfo;
CREATE TABLE stuinfo(
	id INT,
	stuname VARCHAR(20),
	gender CHAR(1),
	seat INT,
	age INT,
	majorid INT,
	
	CONSTRAINT pk PRIMARY KEY(id),#主键
	CONSTRAINT uq UNIQUE(seat),#唯一键
	CONSTRAINT ck CHECK(gender ='男' OR gender  = '女'),#检查
	CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id)#外键
);

SHOW INDEX FROM stuinfo;


#通用的写法:★------------------

CREATE TABLE IF NOT EXISTS stuinfo(
	id INT PRIMARY KEY,
	stuname VARCHAR(20),
	sex CHAR(1),
	age INT DEFAULT 18,
	seat INT UNIQUE,
	majorid INT,
	CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id)
);

主键和唯一的大对比:

保证唯一性 是否允许为空 一个表中可以有多少个 是否允许组合
主键 × 至多有1个 √,但不推荐
唯一 可以有多个 √,但不推荐

外键的特点:

  1. 要求在【从表】设置外键关系
  2. 从表的外键列的类型和主表的关联列的类型要求一致或兼容,名称无要求
  3. 【主表的关联列必须是一个key】(一般是主键或唯一)
  4. 插入数据时,先插入主表,再插入从表
    1. 删除数据时,先删除从表,再删除主表

上面的例子是创建表时添加约束,下面还有修改表时添加约束。

添加约束

在执行CREATE TABLE语句时可以创建索引,也可以单独用`CREATE INDEX`或`ALTER TABLE`来为表增加索引。

​    1、ALTER TABLE
ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
ALTER TABLE table_name ADD INDEX index_name (column_list)
ALTER TABLE table_name ADD UNIQUE (column_list)
ALTER TABLE table_name ADD PRIMARY KEY (column_list)
说明:其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。索引名index_name可选,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

​    2、CREATE INDEX

CREATE INDEX可对表增加普通索引或UNIQUE索引。
CREATE INDEX index_name ON table_name (column_list)
CREATE UNIQUE INDEX index_name ON table_name (column_list)
​    说明:table_name、index_name和column_list具有与ALTER TABLE语句中相同的含义,索引名不可选。另外,不能用CREATE INDEX语句创建PRIMARY KEY索引。

/*
1、添加列级约束
alter table 表名 modify column 字段名 字段类型 新约束;

2、添加表级约束
alter table 表名 add 【constraint 约束名】 约束类型(字段名) 【外键的引用】;
*/
DROP TABLE IF EXISTS stuinfo;
CREATE TABLE stuinfo(
	id INT,
	stuname VARCHAR(20),
	gender CHAR(1),
	seat INT,
	age INT,
	majorid INT
)
DESC stuinfo;
#1.添加非空约束
ALTER TABLE stuinfo MODIFY COLUMN stuname VARCHAR(20)  NOT NULL;
#2.添加默认约束
ALTER TABLE stuinfo MODIFY COLUMN age INT DEFAULT 18;
#3.添加主键
#①列级约束
ALTER TABLE stuinfo MODIFY COLUMN id INT PRIMARY KEY;
#②表级约束
ALTER TABLE stuinfo ADD PRIMARY KEY(id);

#4.添加唯一
#①列级约束
ALTER TABLE stuinfo MODIFY COLUMN seat INT UNIQUE;
#②表级约束
ALTER TABLE stuinfo ADD UNIQUE(seat);

#5.添加外键
ALTER TABLE stuinfo ADD CONSTRAINT fk_stuinfo_major FOREIGN KEY(majorid) REFERENCES major(id); 

删除约束

DROP INDEX index_name ON talbe_name

ALTER TABLE table_name DROP INDEX index_name

ALTER TABLE table_name DROP PRIMARY KEY

1.删除非空约束

ALTER TABLE stuinfo MODIFY COLUMN stuname VARCHAR(20) NULL;

2.删除默认约束

ALTER TABLE stuinfo MODIFY COLUMN age INT ;

3.删除主键

ALTER TABLE stuinfo DROP PRIMARY KEY;

4.删除唯一

ALTER TABLE stuinfo DROP INDEX seat;

5.删除外键

ALTER TABLE stuinfo DROP FOREIGN KEY fk_stuinfo_major;

SHOW INDEX FROM stuinfo;

标识列:

又称自增长列

含义:可以不用手动地插入值,系统提供默认的序列值。

AUTO_INCREMENT

SET AUTO_INCREMENT=2; # 重新设置步长。
需要更改起始值的时候,手动添加一个值即可。
其余时候填入null即可
# 修改表时设置标识列
alter table stu modify column id int primary key auto_increment;
# 修改表时删除标识列
alter table stu modify column id int primary;

问:标识列必须跟主键搭配吗?

答:不一定,但要求是一个key。

问:一个表中可以有多少标识列。

答:一个表中只能有一个标识列。

问:标识列的类型?

答:必须是数值型

DQL语言

查哪些列 查哪张表 查哪些行

DQL(Data Query Language):数据查询语言
select

顺序问题

写的顺序:

SELECT [ALL|DISTINCT] 要查询的字段|表达式select_expr|常量值|函数 
FROM ->
WHERE -> 
GROUP BY [合计函数] -> 
HAVING -> 
ORDER BY -> 
LIMIT
顺序的记法:LIMIT最后,HAVING在两个by中间

执行顺序

FROM
ON JOIN
WHERE
GROUP BY
HAVING
SELECT
DISTINCT
UNION
ORDER BY

FROM

用于标识查询来源。
    -- 可以为表起别名。使用as关键字。
        SELECT * FROM tb1 AS tt, tb2 AS bb;
    -- from子句后,可以同时出现多个表。
        -- 多个表会横向叠加到一起,而数据会形成一个笛卡尔积。
        SELECT * FROM tb1, tb2;
    -- 向优化符提示如何选择索引
        USE INDEX、IGNORE INDEX、FORCE INDEX
        SELECT * FROM table1 USE INDEX (key1,key2) WHERE key1=1 AND key2=2 AND key3=3;
        SELECT * FROM table1 IGNORE INDEX (key3) WHERE key1=1 AND key2=2 AND key3=3;
c. WHERE 子句
    -- 从from获得的数据源中进行筛选。
    -- 整型1表示真,0表示假。
    -- 表达式由运算符和运算数组成。
        -- 运算数:变量(字段)、值、函数返回值
        -- 运算符:
            =, <=>, <>, !=, <=, <, >=, >, !, &&, ||,
            in (not) null, (not) like, (not) in, (not) between and, is (not), and, or, not, xor
            is/is not 加上ture/false/unknown,检验某个值的真假
            <=>与<>功能相同,<=>可用于null比较
            
d. GROUP BY 子句, 分组子句
    GROUP BY 字段/别名 [排序方式]
    分组后会进行排序。升序:ASC,降序:DESC
    
    以下[合计函数]需配合 GROUP BY 使用:
    count 返回不同的非NULL值数目  count(*)、count(字段)
    sum 求和
    max 求最大值
    min 求最小值
    avg 求平均值
    group_concat 返回带有来自一个组的连接的非NULL值的字符串结果。组内字符串连接。
    
e. HAVING 子句,条件子句
    与 where 功能、用法相同,执行时机不同。
    where 在开始时执行检测数据,对原数据进行过滤。
    having 对筛选出的结果再次进行过滤。
    having 字段必须是查询出来的,where 字段必须是数据表存在的。【重要】
    where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。【重要】
    where 不可以使用合计函数。一般需用合计函数才会用 having
    SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列。
    
f. ORDER BY 子句,排序子句
    order by 排序字段/别名 排序方式 [,排序字段/别名 排序方式]...
    升序:ASC,降序:DESC
    支持多个字段的排序。
    
g. LIMIT 子句,限制结果数量子句
    仅对处理好的结果进行数量限制。将处理好的结果的看作是一个集合,按照记录出现的顺序,索引从0开始。
    limit 起始位置, 获取条数
    省略第一个参数,表示从索引0开始。limit 获取条数
    
h. DISTINCT, ALL 选项
    distinct 去除重复记录
    默认为 all, 全部记录

select

①通过select查询完的结果 ,是一个虚拟的表格,不是真实存在
② 要查询的东西 可以是常量值、可以是表达式、可以是字段、可以是函数

数值型和日期型的常量值必须用单引号引起来,数值型不需要

-- 可以用 * 表示所有字段。
        select * from tb;
-- 可以使用表达式(计算公式、函数调用、字段也是个表达式)
        select stu, 29+25, now() from tb;
-- 可以为每个列使用别名。适用于简化列标识,避免多个列标识符重复。
        - 使用 as 关键字,也可省略 as.
        select stu+10 as add10 from tb;
函数
#一、字符函数

#1.l lenth 获取参数值的字节个数
SELECT LENGTH('john');
SELECT LENGTH('张三丰hahaha');

SHOW VARIABLES LIKE '%char%'

#2.concat 拼接字符串
SELECT CONCAT(last_name,'_',first_name) 姓名 FROM employees;

#3.upper、lower
SELECT UPPER('john');
SELECT LOWER('joHn');
#示例:将姓变大写,名变小写,然后拼接
SELECT CONCAT(UPPER(last_name),LOWER(first_name))  姓名 FROM employees;

#4.substr、substring 注意:索引从1开始
#截取从指定索引处后面所有字符
SELECT SUBSTR('李莫愁爱上了陆展元',7)  out_put;

#截取从指定索引处指定字符长度的字符
SELECT SUBSTR('李莫愁爱上了陆展元',1,3) out_put;

#案例:姓名中首字符大写,其他字符小写然后用_拼接,显示出来
SELECT CONCAT(UPPER(SUBSTR(last_name,1,1)),'_',LOWER(SUBSTR(last_name,2)))  out_put
FROM employees;

#5.instr 返回子串第一次出现的索引,如果找不到返回0
SELECT INSTR('杨不殷六侠悔爱上了殷六侠','殷八䩠 'AS out_put;

#6.trim去前后空格
SELECT LENGTH(TRIM('    张翠山    ')) AS out_put;
SELECT TRIM('aa' FROM 'aaaa张aaa翠山aaaaa')  AS out_put;

#7.lpad 用指定的字符实现左填充指定长度
SELECT LPAD('殷素素',2,'*') AS out_put;

#8.rpad 用指定的字符实现右填充指定长度
SELECT RPAD('殷素素',12,'ab') AS out_put;


#9.replace 替换
SELECT REPLACE('周芷若周芷若周芷若周芷若张无忌爱上了周芷若','周芷若','赵敏') AS out_put;
             
#--------------------------------------------------------------
#二、数学函数

#round 四舍五入
SELECT ROUND(-1.55);
SELECT ROUND(1.567,2);


#ceil 向上取整,返回>=该参数的最小整数
SELECT CEIL(-1.02);

#floor 向下取整,返回<=该参数的最大整数
SELECT FLOOR(-9.99);

#truncate 截断
SELECT TRUNCATE(1.69999,1);

#mod取余
/*
mod(a,b) :  a-a/b*b

mod(-10,-3):-10- (-10)/(-3)*(-3)=-1
*/
SELECT MOD(10,-3);
SELECT 10%3;


#三、日期函数

#now 返回当前系统旟+时间
SELECT NOW();

#curdate 返回当前系统日期,不包含时间
SELECT CURDATE();

#curtime 返回当前时间,不包含日期
SELECT CURTIME();


#可以获取指定的部分,年、月、日、小时、分钟、秒
SELECT YEAR(NOW()) 年;
SELECT YEAR('1998-1-1') 年;

SELECT  YEAR(hiredate) 年 FROM employees;

SELECT MONTH(NOW()) 月;
SELECT MONTHNAME(NOW()) 月;

#str_to_date 将字符通过指定的格式转换成日期,中间的符号要一致
SELECT STR_TO_DATE('1998-3-2','%Y-%c-%d') AS out_put;

#查询入职日期为1992--4-3的员工信息
SELECT * FROM employees WHERE hiredate = '1992-4-3';
或
SELECT * FROM employees WHERE hiredate = STR_TO_DATE('4-3 1992','%c-%d %Y');
%Y四位的年份 %y二位的年份 %m二位的月份 %c月份 %d二位的日 %H二十四制小时 %h十二制小时 %i二位制分钟 %s二位制秒

#date_format 将日期转换成字符
SELECT DATE_FORMAT(NOW(),'%Y年%m月%d日') AS out_put;

#查询有奖金的员工名和入职日期(xx月/xx日 xx年)
SELECT last_name,DATE_FORMAT(hiredate,'%m月/%d日 %Y年') 入职日期
FROM employees
WHERE commission_pct IS NOT NULL;


#四、其他函敍
SELECT VERSION();
SELECT DATABASE();
SELECT USER();
一、单行函数
1、字符函数
	length长度,中文是3B
	concat()拼接
	substr截取子串(字符串,开始位置从1开始,长度中文也是1个)
	instr 返回子串第一次出现的索引,如果找不到返回0
	upper转换成大写
	lower转换成小写
	trim去前后指定的空格和字符(字符串,去除内容' ')
	ltrim去左边空格
	rtrim去右边空格
	replace替换(字符串,被替换内容,用什么替换)
	lpad左填充(字符串,填充后的长度,填空内容'')
	rpad右填充
	instr返回子串第一次出现的索引
	length 获取字节个数
	
CONCAT('Hello', 'World') HelloWorld
SUBSTR('HelloWorld',1,5) Hello
LENGTH('HelloWorld') 10
INSTR('HelloWorld', 'W') 6
LPAD(salary,10,'*')  *****24000
RPAD(salary, 10, '*')  24000*****
TRIM('H' FROM 'HelloWorld')  elloWorld
REPLACE('abcd','b','m')  amcd


2、数学函数
	round 四舍五入(数,小数点后保留位数=0)
	rand 随机数
	floor向下取整(小于等于)
	ceil向上取整(大于等于)
	mod取余MOD(10.3)
	truncate截断
3、日期函数
	now()当前系统日期+时间
	curdate()当前系统日期
	curtime()当前系统时间
	str_to_date(,) 将字符转换成日期
	date_format(,)将日期转换成字符

where

where分类:
一、条件表达式
	示例:salary>10000
	条件运算符:
	> < >= <= = != <>不等于,注意等于是=

二、逻辑表达式
示例:salary>10000 && salary<20000
逻辑运算符:
	and(&&):两个条件如果同时成立,结果为true,否则为false
	or(||):两个条件只要有一个成立,结果为true,否则为false
	not(!):如果条件成立,则not后为false,否则为true
# SELECT * FROM employees WHERE name LIKE '%a%' OR salary>100;

三、模糊查询
like
示例:last_name like 'a%'
# SELECT * FROM employees WHERE last_name LIKE '%a%';%代表通配符
通配符:
%任意多个字符
_任一个字符,可以用转义符

between and
SELECT * FROM `employees` WHERE `employee_id` BETWEEN 100小值 AND 120大值;不可以换顺序

in//NOT IN
SELECT * FROM `employees` WHERE job_id IN ['AD_VP','AD_PRES']不可以写通配符,可以写()


SELECT `last_name`,`commission_pct` FROM employees WHERE `commission_pct` IS NOT NULL;等号不能用于判断null值
SELECT `last_name`,`commission_pct` FROM employees WHERE `commission_pct` <=> 120;安全等于。即可以判断NULL,又可以判断数值

group

分组函数作用于一组数据,并每组数据返回一个值。

select 分组函数
from 表
where 条件
group by 
order by 排序的字段|表达式|函数|别名 【asc|desc】

分组函数:
• AVG()
• COUNT(expr) -- 计数,返回expr不为空的记录总数。,适用于任意数据类型。// select count(*) from customer where id=110;
• MAX()
• MIN()
• SUM()

# 在SELECT 列表中所有未包含在组函数中的列都应该包含在 GROUP BY 子句中。
SELECT ID,AVG(salary)
FROM employees
GROUP BY ID;
#包含在 GROUP BY 子句中的列不必包含在SELECT 列表中
SELECT AVG(salary)
FROM employees
GROUP BY ID;

• 不能在 WHERE 子句中使用分组函数。
• 可以在 HAVING 子句中使用分组函数。

分组前筛选: 原始表 group by的前面 where
分组后筛选: 分组后的结果集 group by的后面 having

	sum 求和
	max 最大值(可以算字母顺序,日期顺序)
	min 最小值
	avg 平均值
	count 计数//非空的个数

	特点:
	1、以上五个分组函数都忽略null值,avg也只计算非null,除了count(*)
	2、sum和avg一般用于处理数值型
		max、min、count可以处理任何数据类型
    3、都可以搭配distinct使用,用于统计去重后的结果
	4、count的参数可以支持:
		字段、*、常量值,一般放1

	   建议使用 count(*)统计行数,括号内任意数字或任意常量也可以统计行数
	   
select avg(score) from stu where class='1';
select avg(score) from stu group by class;

#1.查询各job_id的员工工资的最大值,最小值,平均值,总和,并按job_id升序
SELECT MAX(salary),MIN(salary),AVG(salary),SUM(salary),job_id
FROM employees
GROUP BY job_id
ORDER BY job_id;


#2.查询员工最高工资和最低工资的差距(DIFFERENCE)
SELECT MAX(salary)-MIN(salary) DIFFRENCE
FROM employees;
#3.查询各个管理者手下员工的最低工资,其中最低工资不能低于6000,没有管理者的员工不计算在内
SELECT MIN(salary),manager_id
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id
HAVING MIN(salary)>=6000;

#4.查询所有部门的编号,员工数量和工资平均值,并按平均工资降序
SELECT department_id,COUNT(*),AVG(salary) a
FROM employees
GROUP BY department_id
ORDER BY a DESC;
#5.选择具有各个job_id的员工人数
SELECT COUNT(*) 个数,job_id
FROM employees
GROUP BY job_id;

having

使用 HAVING 过滤分组前提:

  1. 行已经被分组。
  2. 使用了组函数。
  3. 满足HAVING 子句中条件的分组将被显示。

不可以使用having的情况:筛选条件的列没有出现在查询select查询字段中

顺序:from on join where group having select(这里顺序是没错的,但我对having和select的关系存疑)

SELECT region, SUM(population), SUM(area)
FROM bbc
GROUP BY region
HAVING SUM(area)>1000 -- 在这里,我们不能用where来筛选超过1000的地区,因为表中不存在这样一条记录。

select sum(score)
from student  
where sex='man'
group by name
having sum(score)>210

having和where的区别

  • having与 where 功能、用法相同,执行时机不同。
  • where 在开始时执行检测数据,对原数据进行过滤。
  • having 对筛选出的结果再次进行过滤。
  • having 字段必须是查询出来的,where 字段必须是数据表存在的。
  • where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。
  • where 不可以使用聚合函数。一般需用聚合函数才会用 having,where执行顺序大于聚合函数
  • where 子句的作用是在对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚组函数,使用where条件显示特定的行。
  • SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列
  • 聚合函数是比较where、having 的关键。若须引入聚合函数来对group by 结果进行过滤 则只能用having
  • having 子句的作用是筛选满足条件的组,即在分组之后过滤数据,条件中经常包含聚组函数,使用having 条件显示特定的组,也可以使用多个分组标准进行分组。

执行顺序:

where >>> 聚合函数(sum,min,max,avg,count) >>> having

order

select 要查询的东西
from 表
where 条件

order by 排序的字段|表达式|函数|别名 【asc|desc】
/*
- ORDER BY 子句在SELECT语句的【结尾】。 

- 【asc|desc】为升降序
- 多个顺序时,写多个即可,逗号分隔
*/

执行顺序:from 、join、 on、 where 、
、group by(开始使用select中的别名,后面的语句中都可以使用)
、avg,sum… having、 select 、distinct 、order by、limit

limit

-- 分页查询

select 字段|表达式,...
from 表
【where 条件】
【group by 分组字段】
【having 条件】
【order by 排序的字段】
limit 【起始的条目索引,最大条目数】;-- 0开始  -- 第二个参数为-1代表打印后面所有
1.起始条目索引从0开始

2.limit子句放在查询语句的最后

3.公式:select * from  表 limit (page-1)*sizePerPage,sizePerPage
假如:
每页显示条目数sizePerPage
要显示的页数 page

- select sname from score where degree=(select max(degree) from score);
这句的启发是:括号里的select的结果是一个值,这样degree=值就是一个where语句,缺点是有可能返回多个等大最大值。即先找到最大值再用where
- 可以用limit实现查取某列第一大的值
select name from stu order by degree limit 0,1; // 从0开始一个值

if case when

if 处理双分支
case语句 处理多分支
	when情况1  then处理等值判断
    when情况2  then处理条件判断
    ELSE
    END
#五、流程控制函数
#1.if函数: if else 的效果IF(表达式,真的话,假的话)

SELECT IF(10<5,'大','小');
SELECT last_name,commission_pct,IF(commission_pct IS NULL,'没奖金,呵呵','有奖金,嘻嘻') 备注
FROM employees;


#2.case函数的使用一: switch case 的效果
/*
java中
switch(变量或表达式){
	case 常量1:语句1;break;
	...
	default:语句n;break;
}
mysql中:
case 要判断的字段或表达式
when 常量1 then 要显示的值1或语句1;
when 常量2 then 要显示的值2或语句2;
...
else 要显示的值n或语句n;
end
*/

/*案例:查询员工的工资,要求
部门号=30,显示的工资为1.1倍
部门号=40,显示的工资为1.2倍
部门号=50,显示的工资为1.3倍
其他部门,显示的工资为原工资
*/
SELECT salary 原始工资,department_id,
CASE department_id
WHEN 30 THEN salary*1.1
WHEN 40 THEN salary*1.2
WHEN 50 THEN salary*1.3
ELSE salary 
END AS 新工资
FROM employees;

#3.case 函数的使用二:类似于 多重if
/*
mysql中:
case[case后没有语句直接when]
when 条件1 then 要显示的值1或语句1
when 条件2 then 要显示的值2或语句2
。。。
else 要显示的值n或语句n
end
*/

#案例:查询员工的工资的情况
如果工资>20000,显示A级别
如果工资>15000,显示B级别
如果工资>10000,显示C级别
否则,显示D级别


SELECT salary,
CASE 
WHEN salary>20000 THEN 'A'
WHEN salary>15000 THEN 'B'
WHEN salary>10000 THEN 'C'
ELSE 'D'
END AS 工资级别
FROM employees;

join

等值连接、非等值连接 (内连接)
外连接
交叉连接

语法:

select 字段,...
from 表1
【inner|left outer|right outer|cross】join 表2 on  连接条件
【inner|left outer|right outer|cross】join 表3 on  连接条件
【where 筛选条件】
【group by 分组字段】
【having 分组后的筛选条件】
【order by 排序的字段或表达式】

/* 连接查询(join) */ ------------------
    将多个表的字段进行连接,可以指定连接条件。
-- 内连接(inner join)
    - 默认就是内连接,可省略inner。
    - 只有数据存在时才能发送连接。即连接结果不能出现空行。
    on 表示连接条件。其条件表达式与where类似。也可以省略条件(表示条件永远为真)
    也可用where表示连接条件。
    还有 using, 但需字段名相同。 using(字段名)
    -- 交叉连接 cross join
        即,没有条件的内连接。
        select * from tb1 cross join tb2;
-- 外连接(outer join)
    - 如果数据不存在,也会出现在连接结果中。
    -- 左外连接 left join
        如果数据不存在,左表记录会出现,而右表为null填充
    -- 右外连接 right join
        如果数据不存在,右表记录会出现,而左表为null填充
-- 自然连接(natural join)
    自动判断连接条件完成连接。
    相当于省略了using,会自动查找相同字段名。
    natural join
    natural left join
    natural right join
select info.id, info.name, info.stu_num, extra_info.hobby, extra_info.sex from info, extra_info where info.stu_num = extra_info.stu_id;

在使用left join时,on和where条件的区别如下:

1、on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。

2、where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉

left join的工作机制不在于on条件有几个,也不在于on条件关联的是哪个字段。而是重点在于【如果对于左面的一行没有一个on条件满足的话就自动填充一行null】

在工作中还遇到过这么一个需求:查出来null要有默认值。你可以使用IFNULL(字段,默认值)coalesce(判断式1,判断式2...)他俩的工作机制是不一样的,但是可以达到一样的效果

先进行on过滤,后进行where过滤,记住前面的顺序是from on join where

假设有两张表:

表1:tab2

id size
1 10
2 20
3 30

表2:tab2

size name
10 AAA
20 BBB
20 CCC

两条SQL:

1、select * form tab1 left join tab2 on (tab1.size = tab2.size) where tab2.name=’AAA’
2、select * form tab1 left join tab2 on (tab1.size = tab2.size and tab2.name=’AAA’)

1、第一条SQL的过程:select * form tab1 left join tab2 on (tab1.size = tab2.size) where tab2.name=’AAA’

1.1、中间表
on条件: tab1.size = tab2.size

ab1.id tab1.size tab2.size tab2.name
1 10 10 AAA
2 20 20 BBB
2 20 20 CCC
3 30 (null) (null)

1.2、再对中间表过滤
where 条件:tab2.name=’AAA’

tab1.id tab1.size tab2.size tab2.name
1 10 10 AAA

2、第二条SQL的过程:select * form tab1 left join tab2 on (tab1.size = tab2.size and tab2.name=’AAA’)

tab1.id tab1.size tab2.size tab2.name
1 10 10 AAA
2 20 (null) (null)
3 30 (null) (null)

其实以上结果的关键原因就是left join,right join,full join的特殊性,不管on上的条件是否为真都会返回left或right表中的记录,full则具有left和right的特性的并集。 而inner jion没这个特殊性,则条件放在on中和where中,返回的结果集是相同的。

子查询

/* 子查询 */
    - 子查询需用括号包裹。
-- from型
    from后要求是一个表,必须给子查询结果取个别名。
    - 简化每个查询内的条件。
    - from型需将结果生成一个临时表格,可用以原表的锁定的释放。
    - 子查询返回一个表,表型子查询。
    select * from (select * from tb where id>0) as subfrom where id>1;
-- where型
    - 子查询返回一个值,标量子查询。
    - 不需要给子查询取别名。
    - where子查询内的表,不能直接用以更新。
    select * from tb where money = (select max(money) from tb);
    -- 列子查询
        如果子查询结果返回的是一列。
        使用 in 或 not in 完成查询
        exists 和 not exists 条件
            如果子查询返回数据,则返回1或0。常用于判断条件。
            select column1 from t1 where exists (select * from t2);
    -- 行子查询
        查询条件是一个行。
        select * from t1 where (id, gender) in (select id, gender from t2);
        行构造符:(col1, col2, ...) 或 ROW(col1, col2, ...)
        行构造符通常用于与对能返回两个或两个以上列的子查询进行比较。
    -- 特殊运算符
    != all()    相当于 not in
    = some()    相当于 in。any 是 some 的别名
    != some()   不等同于 not in,不等于其中某一个。
    all, some 可以配合其他运算符一起使用。

子查询:内层被嵌套的select

  • 子查询都放在小括号内
  • 子查询可以放在from后面、select后面、where后面、having后面,但一般放在条件的右侧
  • 查询结果:
    • 结果集只有一行:一般搭配单行操作符使用:> < = <> >= <=
    • 结果集有多行:一般搭配多行操作符使用:any、all、in、not in

主查询:外层的select

union

1、多条查询语句的查询的列数必须是一致的
2、多条查询语句的查询的列的类型几乎相同
3、union代表去重,union all代表不去重

select 字段|常量|表达式|函数 【from 表】 【where 条件】 union 【all】

select 字段|常量|表达式|函数 【from 表】 【where 条件】 union 【all】
select 字段|常量|表达式|函数 【from 表】 【where 条件】 union  【all】
.....
select 字段|常量|表达式|函数 【from 表】 【where 条件】


/* UNION */ ------------------
    将多个select查询的结果组合成一个结果集合。
    SELECT ... UNION [ALL|DISTINCT] SELECT ...
    默认 DISTINCT 方式,即所有返回的行都是唯一的
    建议,对每个SELECT查询加上小括号包裹。
    ORDER BY 排序时,需加上 LIMIT 进行结合。
    需要各select查询的字段数量一样。
    每个select查询的字段列表(数量、类型)应一致,因为结果中的字段名以第一条select语句为准。

TRUNCATE

/* TRUNCATE */ ------------------
TRUNCATE [TABLE] tbl_name
清空数据
删除重建表
区别:
1,truncate 是删除表再创建,delete 是逐条删除
2,truncate 重置auto_increment的值。而delete不会
3,truncate 不知道删除了几条,而delete知道。
4,当被用于带分区的表时,truncate 会保留分区

MyISAM和InnoDB区别

MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。

两者的对比:

  1. 是否支持行级锁 : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁。
  2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外键: MyISAM不支持,而InnoDB支持。
  4. 是否支持MVCC :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 读已提交READ COMMITTED可重复读REPEATABLE READ 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读:MySQL-InnoDB-MVCC多版本并发控制

《MySQL高性能》:不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。

一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。

事务

Transaction Control Language 事务控制语言

事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。

  • 支持连续SQL的集体成功或集体撤销。
  • 事务是数据库在数据完整性方面的一个功能。
  • 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。
  • InnoDB被称为事务安全型引擎。

1 ACID

  • 原子性Atomicity:要么都执行,要么都回滚。(事务不可分割)
  • 一致性Consistency:保证数据的状态操作前和操作后保持一致。数据库总是从一个一致性的状态转换到另一个一致性的状态。事务前后数据的完整性必须保持一致。
    • 事务前后数据的完整性必须保持一致。
    • 在整个事务过程中,操作是连续的
  • 隔离性Isolation:多个事务同时操作相同数据库的同一个数据时,一个事务的执行不受另外一个事务的干扰。“通常来说”(有隔离级别的区别),一个事务所做的修改在最终提交之前,对其他事务是不可见的。
  • 持久性Durability:一个事务一旦提交,则数据将持久化到本地,除非其他事务对其进行修改

事务的分类:

  • 隐式事务,没有明显的开启和结束事务的标志:比如insert、update、delete语句本身就是一个事务
  • 显式事务,具有明显的开启和结束事务的标志。开启事务、编写事务逻辑、提交/回滚事务

2 事务命令

-- 开启事务有下面3种方式:
- START TRANSACTION; -- 临时一次事务
- begin -- 临时一次事务
- set autocommit=0 -- 永久变为事务
开启事务后,所有被执行的SQL语句均被认作当前事务内的SQL语句。

set autocommit=0;
start transaction;
...
commit;-- 事务提交
rollback; -- 事务回滚


savepoint  断点 # 可以回滚 # 在事务的过程中使用
commit to 断点
rollback to 断点

-- 事务的原理
    利用InnoDB的自动提交(autocommit)特性完成。
    普通的MySQL执行语句后,当前的数据提交操作均可被其他客户端可见。
    而事务是暂时关闭“自动提交”机制,需要commit提交持久化数据操作。
-- 注意
    1. 数据定义语言(DDL)语句不能被回滚,比如创建或取消数据库的语句,和创建、取消或更改表或存储的子程序的语句。
    2. 事务不能被嵌套
-- 保存点
    SAVEPOINT 保存点名称 -- 设置一个事务保存点
    ROLLBACK TO SAVEPOINT 保存点名称 -- 回滚到保存点
    RELEASE SAVEPOINT 保存点名称 -- 删除保存点
-- InnoDB自动提交特性设置
    SET autocommit = 0|1;   0表示关闭自动提交,1表示开启自动提交。
    - 如果关闭了,那普通操作的结果对其他客户端也不可见,需要commit提交后才能持久化数据操作。
    - 也可以关闭自动提交来开启事务。但与START TRANSACTION不同的是,
        SET autocommit是永久改变服务器的设置,直到下次再次修改该设置。(针对当前连接)
        而START TRANSACTION记录开启前的状态,而一旦事务提交或回滚后就需要再次开启事务。(针对当前事务)

3 事务隔离级别:

  • READ UNCOMMITTED:读未提交:这个事务修改了数据,即使没有提交,其他事务也可以读到修改后的值,这也被称为脏读。这个级别会导师制很多问题,从性能上来说也没有比其他级别好太多,所以除非真的必要,实际中很少使用
    • 脏读:T1读取到了T2未提交的数据。【更新】
  • READ COMMITTED:读已提交。可以避免脏读。这个级别也叫做不可重复度,因为两次执行同样的查询,可能会得到不同的结果。
    • 不可重复读:T1,T2读取了同一字段,T2改后,T1再读发现不一样了。脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了其他事务已提交的数据。需要注意的是在某些情况下不可重复读并不是问题。
  • REPEATABLE READ==(Mysql默认)==:可重复读:可以避免脏读、不可重复读。但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。Mysql默认使用该隔离级别。这可以通过“共享读锁”和“排他写锁”实现,即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成并不立刻释放 S 锁,而是等到事物结束后再释放。
    • 幻读phantom read:T1打开事务后select只发现3行,然后T1暂停,T2 insert后提交,T1再update table1 set '111’发现更新了4行。
    • 幻读:(自己先全局修改,别人再添加,自己最后查询)即例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读可能发生在update,delete操作中,而幻读发生在insert操作中。
    • 幻读最根本的原因:发生了MVCC的"当前读"
    • 如何防止幻读:去看MVCC和间隙锁吧
  • SERIALIZABLE可以避免脏读、不可重复读和幻读

锁的类型

  • 行锁只是一个比较泛的概念,在innodb存储引擎中,行锁的实现主要包含三种算法:
    1. Record-Key Lock:单个数据行的锁,锁住单条记录;
    2. Gap Lock:间隙锁,锁住一个范围,但是不包含数据行本身;
    3. Next-Key Lock:Record-Key Lock + Gap Lock,锁住数据行本身和一个范围的数据行。
  • 所以innodb的行锁不是简单的锁住某一个数据行这个单条记录,而是根据更新条件,如WHERE中可能包含 > 等范围条件,和事务隔离级别来确定是锁住单条还是多条数据行。
事务隔离级别 脏读 不可重复度 幻读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITTED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许
隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

设置隔离级别:

# 设置隔离级别
set session|global  transaction isolation level 隔离级别名;
# 查看隔离级别
select @@tx_isolation;

4 Mysql中的事务

自动提交

mysql默认采用自动提交auto commit的模式,即一条语句输入完后立即执行,不等后一句输入。

show variables lick 'autocommit'; -- 可以查询自动提交状态

-- 关闭自动提交
set autocommit=1
修改该值对非事务型的表,如MyISAM或者内存表,不会有任何影响。因为他们没有COMMIT和ROLLBACK的概念,也可以说相当于一只处于autocommit启用的模式。
强制自动提交

还有一些命令,也会强制执行COMMIT提交当前的活动事务。如数据定义语言DDL中,如果是导致大量数据改变的操作,如ALTER TABLE、LOCK TABLE,会自动提交之前的语句

隐式和显式锁定

InnoDB采用的是两阶段锁定协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻释放。前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。

另外InnoDB也支持通过特定的语句进行显示锁定,这些语句不属于SQL规范

select ...LOCK IN SHARE MODE; -- 读锁

select ...FOR UPDATE; -- 写锁

LBCC

InnoDB默认的事务隔离级别是repeatable read(后文中用简称 RR),它为了解决该隔离级别下的幻读的并发问题,提出了LBCCMVCC两种方案。

  • 其中LBCC解决的是当前读情况下的幻读
  • MVCC解决的是普通读(快照读)的幻读

至于什么是当前读,什么是快照读,将在下文中给出答案。

LBCC基于锁

注意,LBCC是基于锁的,这句话的意思是说,你使用了for update等语句时才会进入LBCC模式。

LBCC还有一个前提是走了索引,如果没走索引那么使用select …for update锁的是表锁

LBCCLock-Based Concurrent Control的简称,意思是基于锁的并发控制

InnoDB中按锁的模式来分的话可以分为共享锁(S)、排它锁(X)和意向锁,其中意向锁又分为意向共享锁(IS)和意向排它锁(IX)(此处先不做介绍,后期会专门出篇文章讲一下InnoDBMyisam引擎的锁);

如果按照锁的算法来分的话又分为记录锁(Record Locks)、间隙锁(Gap Locks)和临键锁(Next-key Locks)。其中临键锁就可以用来解决 RR 下的幻读问题。那么什么是临键锁呢?继续往下看。

【数据库】1、MySQL、事务、MVCC、LBCC_第1张图片

我们将数据库中存储的每一行数据称为记录。则上图中

  • 1、5、9、11 分别代表 id 为当前数的记录。
  • 对于键值在条件范围内但不存在的记录,叫做间隙(GAP)。则上图中的(-∞,1)、(1,5)…(11,+∞)为数据库中存在的间隙。
  • 而(-∞,1]、(1,5]…(11,+∞)我们称之为临键,即左开右闭的集合。

1、记录锁(Record Locks)

对表中的行记录加锁,叫做记录锁,简称行锁。可以使用sql语句select ... for update来开启锁,select语句必须为精准匹配(=),不能为范围匹配,且匹配列字段必须为唯一索引或者主键列。也可以通过对查询条件为主键索引或唯一索引的数据行进行UPDATE操作来添加记录锁。

记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。

2、间隙锁(GAP Locks)

间隙锁(Gap Lock)是Innodb在[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0kA1Coa-1629816198886)(https://math.jianshu.com/math?formula=%5Ccolor%7Bred%7D%7B%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%7D)]提交下为了解决幻读问题时引入的锁机制,(下面的所有案例没有特意强调都使用可重复读隔离级别)幻读的问题存在是因为新增或者更新操作,这时如果进行范围查询的时候(加锁查询),会出现不一致的问题,这时使用不同的行锁已经没有办法满足要求,需要对一定范围内的数据进行加锁,间隙锁就是解决这类问题的。在可重复读隔离级别下,数据库是通过行锁和间隙锁共同组成的(next-key lock),来实现的。

间隙锁是对范围加锁,但不包括已存在的索引项。可以使用sql语句select ... for update来开启锁,

总结:使用for update就禁止其他线程操作该记录了

GAP Locks只存在于 RR 隔离级别下,它锁住的是间隙内的数据。加完锁之后,间隙中无法插入其他记录,并且锁的是记录间隙,而非sql语句。间隙锁之间都不存在冲突关系。

下面情况会加上间隙锁:

  • select语句为范围查询,匹配列字段为索引项,且没有数据返回
  • 或者select语句为等值查询,匹配字段为唯一索引,也没有数据返回

为什么这么做:前面说了是为了防止当前读产生的幻读,所以就自己加锁不让别人访问,如果有指定的记录那么直接就锁住了没问题,但是没有记录呢?不能因为当前读就产生幻读读到别事务插入的内容吧,所以就锁住区间,“我没查到你也别插入,省得我当前读后疑惑”,但是加锁的范围多大呢?这个就是间隙锁要关注的

间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。以下是加锁之后,插入操作的例子:

【数据库】1、MySQL、事务、MVCC、LBCC_第2张图片
select * from user where id > 15 for update;

# 其他事务执行
# 插入失败,因为id=20大于15,不难理解
insert into user values(20,'20');
# 插入失败,原因是间隙锁锁的是记录间隙,而不是sql,也就是说`select`语句的锁范围是(11,+∞),而13在这个区间中,所以也失败。
insert into user values(13,'13');

打开间隙锁设置: 以通过命令show variables like 'innodb_locks_unsafe_for_binlog';来查看 innodb_locks_unsafe_for_binlog 是否禁用。innodb_locks_unsafe_for_binlog默认值为 OFF,即启用间隙锁。因为此参数是只读模式,如果想要禁用间隙锁,需要修改 my.cnf(windows 是my.ini) 重新启动才行。

#在 my.cnf 里面的[mysqld]添加
[mysqld]
innodb_locks_unsafe_for_binlog = 1

3、临键锁(Next-Key Locks)

当我们对上面的记录和间隙共同加锁时,添加的便是临键锁(左开右闭的集合加锁)。为了防止幻读,临键锁阻止特定条件的新记录的插入,因为插入时要获取插入意向锁,与已持有的临键锁冲突。可以使用sql语句select ... for update来开启锁,

  • select语句为范围查询,匹配列字段为索引项,且数据返回;
  • 或者select语句为等值查询,匹配列字段为索引项,不管有没有数据返回。

插入意向锁并非意向锁,而是一种特殊的间隙锁。

总结

下面分为3个情况考虑:没有命中索引、走普通索引、走唯一索引

下面最简单的是唯一索引,没有命中锁间隙所就行,用不到临建锁

  • 如果查询没有命中索引,则退化为表锁;
  • 唯一索引
    • 如果等值查询唯一索引且命中唯一一条记录,则退化为行锁;
    • 如果等值查询唯一索引且没有命中记录,则退化为临近结点的间隙锁;
    • 如果范围查询唯一索引或查询非唯一索引且命中记录,则锁定所有命中行的临键锁 ,并同时锁定最大记录行下一个区间的间隙锁。(说得比较复杂,其实就是说左右找到确定不包含的唯一值,然后把中间的都锁住)
  • 如果等值查询非唯一索引且没有命中记录,退化为临近结点的间隙锁(包括结点也被锁定);如果命中记录,则锁定所有命中行的临键锁,并同时锁定最大记录行下一个区间的间隙锁。
  • 如果范围查询索引且没有命中记录,退化为临近结点的间隙锁(包括结点也被锁定)。

MVCC

参考:(推荐)https://blog.csdn.net/SnailMann/article/details/94724197

https://juejin.im/post/6844903799534911496

https://segmentfault.com/a/1190000012650596

多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交可重复读隔离级别的实现。可以认为MVCC是行级锁的一个变种。

MySQL就利用了MVCC来判断在一个事务中,哪个数据可以被读出来,哪个数据不能被读出来。实现对数据库的并发访问。

多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写、写读、写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undo log中实现的,通过undo log可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

LBCC是基于锁的并发控制,因为锁的粒度过大,会导致性能的下降,因此提出了比LBCC性能更优越的方法MVCCMVCCMulti-Version Concurremt Control的简称,意思是基于多版本的并发控制协议,通过版本号,避免同一数据在不同事务间的竞争,只存在于InnoDB引擎下。它主要是为了提高数据库的并发读写性能,不用加锁就能让多个事务并发读写。MVCC的实现依赖于:三个隐藏字段、Undo logRead View,其核心思想就是:只能查找事务 id 小于等于当前事务 ID 的行;只能查找删除时间大于等于当前事务 ID 的行,或未删除的行。接下来让我们从源码级别来分析下MVCC

MVCC只在 READ COMMITTEDREPEATABLE READ两个隔离级别下工作,其他两个隔离级别不和MVCC不兼容。

因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行,

SERIALIZABLE 则会对所有读取的行都加锁。事务的快照时间点(即下文中说到的Read View的生成时间)是以第一个select来确认的。所以即便事务先开始,但是select在后面的事务的update之类的语句后进行,那么它是可以获取前面的事务的对应的数据。

快照读和当前读

提醒:阅读MVCC和快照读的时候要注意区分是在上面隔离级别下

MySQL InnoDB下的当前读快照读?

说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

出现了上面的情况我们需要知道为什么会出现这种情况。通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据

  • 这种读取历史数据的方式,我们叫它快照读 (snapshot read),
  • 读取数据库最新版本数据的方式,叫当前读 (current read)

RC 和 RR 隔离级别下的快照读和当前读:

RC 隔离级别下,快照读和当前读结果一样,都是读取已提交的最新;

RR 隔离级别下,当前读结果是其他事务已经提交的最新结果,快照读是读当前事务之前读到的结果。RR 下创建快照读的时机决定了读到的版本。

快照读select

当执行select操作(不加锁时)是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。

不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

快照的生成是在第一次执行select的时候,也就是说假设当开启了事务A,然后没有执行任何操作,这时候事务B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。

事务1 事务2 说明
begin; begin;
insert into test values(null,‘D’)
commit;
select * from test; - 事务1能查看插入的D,此时生成快照
commit;

如何让select读的时候别的事务无法更改这些行:

select的当前读需要手动地加锁:(不加锁就是快照读)

select * from table where ? lock in share mode;
select * from table where ? for update;
# 其实上面这些操作依据是当前读了
当前读

当前读(Locking Read)也称锁定读,读取当前数据的最新版本,而且读取到这个数据之后会对这个数据加锁,防止别的事务更改即通过next-key锁(行锁+gap 锁)来解决当前读的问题。在进行写操作的时候就需要进行“当前读”,读取数据记录的最新版本,包含以下SQL类型:select ... lock in share modeselect ... for updateupdatedeleteinsert

对于会对数据修改的操作(updateinsertdelete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致上面(可重复读级别下幻读)我们测试的那种情况。

  • 在可重复读隔离级别下,普通查询select是快照读,是不会看到别的事务插入的数据的,幻读只有发生过当前读才会出现
  • 幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有已经提交记录的最新值

如下操作是当前读

  • select lock in share mode(共享锁),
  • select for update; (排他锁)
  • update, insert ,delete(排他锁)

为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录(要获取写锁),会对读取的记录进行加锁。如果别的事务已经修改了但没提交,当前事务就会卡住阻塞等别的事务commit

在数据库的增、删、改、查中,只有增、删、改才会加上排它锁,而只是查询并不会加锁,只能通过在select语句后显式加lock in share mode或者for update来加共享锁或者排它锁

假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。

事务 1 事务 2 说明
begin begin
- select * from test 1还没开启事务
- insert into test(name) values(“E”) 2生成写锁
select * from test - 1没卡住,快照读不卡
delete from test where name=‘E’; - 1卡住了,当前读是冲突的
阻塞放行 commit 1放行,2提交后2的写锁就取消了,1就拿到了写锁
commit

利用MVCC解读幻读

https://blog.csdn.net/weixin_33755554/article/details/93881494

幻读

CREATE TABLE `author` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

INSERT into author VALUES (1,'g1',20),(5,'g5',20),(15,'g15',30),(20,'g20',30);
# 不可重复读级别

查找年龄为20岁的作者,并把姓名改成G0

【数据库】1、MySQL、事务、MVCC、LBCC_第3张图片

来分析下情形:

  • T1时刻 读取年龄为20的数据, Session1拿到了2条记录。
  • T2时刻 另一个进程Session2插入了一条新的记录,年龄也为20
  • T3时刻,Session1再次读取年龄为20的数据,发现还是2条数据,貌似 Session2新插入的数据并未影响到Session1的事务读取。

对于T1 – T3 时刻的情形,从结果来看,在可重复度读隔离级别下似乎解决了幻读的问题。

  • T4时刻,Session1 修改年龄为20的数据, 发现影响行数为3条。 为什么T3时候只能查到2条数据,但现在修改确修改了3条数据?
  • T5时刻,Session1 再次读取年龄为20的数据,发现结果变成了3条,我们知道被修改的第三条就是Session2在T2时刻新增的一条。

T4,T5 的结果来看,Session1 读到了 Session2 新插入的数据。产生了幻读现象

总结:按理说我应该只会更新2行,却更新了3行。

到底可重复读隔离级别下,解决了幻读问题没有?

了解过MVCC的同学,肯定知道或听说过当前读,和快照读。首先要知道的是MVCC 就InnoDB 秒级建立数据快照的能力。 快照读就是读取数据的时候会根据一定规则读取事务可见版本的数据。 而当前读就是读取最新版本的数据。什么情况下使用的是快照读:(快照读,不会加锁)

一般的 select * from … where … 语句都是快照读

什么情况下使用的是当前读:(当前读,会在搜索的时候加锁)

select * from … where … for update

select * from … where … lock in share mode

update … set … where …

delete from. . where …

如果事务中都使用快照读,那么就不会产生幻读现象,但是快照读和当前读混用就会产生幻读。

如果都是使用当前读,能不能解决幻读问题?

先让我们数据恢复到初始状态

TRUNCATE TABLE author;
INSERT into author VALUES (1,'g1',20),(5,'g5',20),(15,'g15',30),(20,'g20',30);

【数据库】1、MySQL、事务、MVCC、LBCC_第4张图片

可以看到Session 2 被阻塞了。需要等到Session1 提交事务后才能完成。当我们在事务中每次读取都使用当前读,也就是人工把InnoDB变成了串行化。一定程度上降低了并发性,但是也同样避免了幻读的情况。

当前读为什么会阻塞新数据的插入,主要是间隙锁的加锁机制。

幻读前提

前提:InnoDB引擎,可重复读隔离级别下,使用过当前读时。

  • 可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只在当前读下才会出现
  • 幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有已经提交记录的最新值。
事务 1 事务 2 说明
begin begin 假设现有数据一行A
select * from test
- insert into test(name) values(“B”)
- commit 2提交后1还是只有1条
update test set name=“C” 此时1再查询就已经2条C了
commit 保存了C的记录
如果把上个commit换成rollback 查询结果还是B

如下,我们永远锁的是当前d=5的数据,而无法锁新插入d=5的数据,所以再次当前读 读数据 的时候会发现跟原来读的不一样

【数据库】1、MySQL、事务、MVCC、LBCC_第5张图片

幻读的影响:

  • 会造成一个事务中先产生的锁,无法锁住后加入的满足条件的行
  • 产生数据一致性问题,在一个事务中,先对符合条件的目标行做变更,而在事务提交前有新的符合目标条件的行加入,这样通过binlog恢复的数据是会将所有符合条件的目标行都进行变更的。
如何解决幻读:
  • 将两行记录间的空隙加上锁,组合新记录的插入;这个锁称为间隙锁
  • 间隙锁和间隙锁之间没有冲突。跟间隙锁存在冲突关系的是"往这个间隙锁中插入一个记录"这个操作
原值
id  name
1   A
-----------
理论值
id  name
1   C
2   B
-----------
实际值
id  name
1   C
2   C
-----------
本来我们希望得到的结果只是第一条数据的改为C,但是结果却是两条数据都被修改C了。
这种结果告诉我们其实在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。

怎么用READVIEW的理论解释:

先介绍两个概念:

  • 系统版本号:一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号:事务开始时的系统版本号。

MVCC的实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突MVCC的实现原理主要是依赖记录中的 3个隐式字段undo日志Read View 来实现的。所以我们先来看看这个三个关键点的概念

快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现

①行格式简介

主要说行格式里的隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。(在InnoDB引擎表中,它的聚簇索引记录中。)

【数据库】1、MySQL、事务、MVCC、LBCC_第6张图片

  • DB_TRX_ID
    6byte,最近修改(修改/插入)事务ID:记录创建这条记录/或最后一次修改该记录的事务ID
  • DB_ROLL_PTR
    7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)。roll_pointer。每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
  • DB_ROW_ID
    6byte,隐含的自增ID(隐藏主键),如果数据表没有主键和非空唯一主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。
    • 如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式地定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
  • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

比如我们原有数据

id name transaction_ID roll_pointer
1 A 5 上一个版本的地址

比如执行了一条更新操作

# 事务id=6
update test set name='B' where id=1;
id name transaction_id roll_pointer
1 B 6 指向本条记录的旧版本id=5
1 A 5

具体的执行过程

begin->用排他锁锁定该行->记录redo log->记录undo log->修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行

上述过程确切地说是描述了UPDATE的事务过程,其实undo log分insert和update undo log,因为insert时,原始的数据并不存在,所以回滚时把insert undo log丢弃即可,而update undo log则必须遵守上述过程

②undo日志

每当我们要对一条记录做改动时(这里的改动可以指 INSERT、DELETE、UPDATE),都需要把回滚时所需的东西记录下来, 比如:

undo log主要分为两种:

  • insert undo log
    • 代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
    • 插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了
  • Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
  • update undo log
    • 事务在进行updatedelete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
    • 修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。InnoDB把这些为了回滚而记录的这些东西称之为undo log。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log

每次对记录进行改动都会记录一条 undo 日志,每条 undo 日志也都有一个DB_ROLL_PTR属性,可以将这些 undo 日志都连起来,串成一个链表,形成版本链。版本链的头节点就是当前记录最新的值。

先插入一条记录,假设该记录的事务 id 为 80,那么此刻该条记录的示意图如下所示

【数据库】1、MySQL、事务、MVCC、LBCC_第7张图片

实际上insert undo只在事务回滚时起作用,当事务提交后,该类型的 undo 日志就没用了,它占用的Undo Log Segment也会被系统回收。接着继续执行 sql 操作

【数据库】1、MySQL、事务、MVCC、LBCC_第8张图片

其版本链如下

【数据库】1、MySQL、事务、MVCC、LBCC_第9张图片

很多人以为undo log用于将数据库物理的恢复到执行语句或者事务之前的样子,其实并非如此,undo log是逻辑日志,只是将数据库逻辑的恢复到原来的样子。因为在多并发系统中,你把一个页中的数据物理的恢复到原来的样子,可能会影响其他的事务。

purge线程

  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己还维护了一个read view(这个read view相当于系统中最老活跃事务的read view);
  • 如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见(这里应该是说事务提交了就是可见的),那么这条记录一定是可以被安全清除的。
  • 大多数对数据的变更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除(谁会对刚插入的数据有可见性需求呢!!),而对于UPDATE/DELETE则需要维护多版本信息,在InnoDB里,UPDATE和DELETE操作产生的Undo日志被归成一类,即update_undo
  • 另外, 在回滚段中的undo logs分为: insert undo logupdate undo log
    • insert undo log : 事务对insert新记录时产生的undolog, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃
    • update undo log : 事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

https://segmentfault.com/a/1190000012650596

InnoDB存储引擎在数据库每行数据的后面添加了三个字段

  • 6字节的事务ID(DB_TRX_ID)字段: 用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。
    至于delete操作,在innodb看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除
  • 7字节的回滚指针(DB_ROLL_PTR)字段: 指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。
    如果一行记录被更新, 则 undo log record 包含 ‘重建该行记录被更新之前内容’ 所必须的信息。
  • 6字节的DB_ROW_ID字段: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
    结合聚簇索引的相关知识点, 我的理解是, 如果我们的表中没有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 但聚簇索引会使用DB_ROW_ID的值来作为主键; 如果我们有自己的主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID 了 。
    关于聚簇索引, 《高性能MySQL》中的篇幅对我来说已经够用了, 稍后会整理一下以前的学习笔记, 然后更新上来。

对MVCC有帮助的实质是update undo logundo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

一、 比如一个有个事务插入person表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID回滚指针,我们假设为NULL(这里应该会产生一条insert undo log)

img

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 事务1修改该行(记录)数据时,数据库会先对该行加排他锁(自动加,事务提交后时释放)
  • 然后把该行数据拷贝到undo log中(应该是update undo log),作为旧记录,既在undo log中有当前行的拷贝副本(修改前的)。(unlog是每行有一个log还是全部行共享一个log无所谓,反正有指针串联)
  • 拷贝完毕后,修改该行name为Tom(数据库中的),并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁

【数据库】1、MySQL、事务、MVCC、LBCC_第10张图片

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到undo log中(update undo log),作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头(头插),插在该行旧记录的undo log最前面
  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
  • 事务提交,释放锁

【数据库】1、MySQL、事务、MVCC、LBCC_第11张图片

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里

③ReadView

已提交读和可重复读的区别就在于它们生成ReadView的策略不同。

我觉得每个事务都有自己的read view

ReadView:ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务

什么是Read View?

什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前行事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本

前面说了purge线程用来清理没有用的记录,为了能让这个功能工作,它维护着read view。

拿着当前事务id可以追踪要查询行的undo log链表,从首到尾找到小于等于该事务id的记录即可(其实这么说不对,后面我们再探究)。

purge线程

  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己还维护了一个read view(这个read view相当于系统中最老活跃事务的read view);
  • 如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见(这里应该是说事务提交了就是可见的),那么这条记录一定是可以被安全清除的。

通过这个列表来判断记录的某个版本是否对当前事务可见,“我到底可以读取这个数据的哪个版本”。

那么这个判断条件是什么呢?
【数据库】1、MySQL、事务、MVCC、LBCC_第12张图片
我们这里盗窃@呵呵一笑百媚生一张源码图,如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法(不完全哈,但能看出大致逻辑),该方法展示了我们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较

ReadView组成

ReadView包含四个比较重要的内容:

  • m_ids:生成ReadView时,系统中活跃的事务{id}集合trx_list(名字我随便取的)。
    • 这个的意义在于,如果当前事务创建时别的事务还没提交,即使当前事务的id大,也不能读取那些没有提交的事务(事务id小)
  • up_limit_id:上面{id}中的最小值。
    • 小于这个事务id的undo log肯定能看到
  • low_limit_id:生成ReadView时,系统应该分配给下一个事务的id。也就是目前已出现过的事务ID的最大值+1
    • 大于这个事务id的undo log肯定看不到
  • creator_trx_id:生成该ReadView的事务id。

【数据库】1、MySQL、事务、MVCC、LBCC_第13张图片

  1. 如果落在绿色部分(trx_id
  2. 如果落在红色部分(trx_id>max_id),表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分(min_id<=trx_id<=max_id),那就包含两种情况:a.若 row 的 trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;如果是自己的事务,则是可见的;b.若 row 的 trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

查看当前所有的未提交并活跃的事务,存储在数组中
选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
选取所有已提交事务中最大的XID,加1后记录在xmax中

img

使用方法:

  • 比较当前行的事务idDB_TRX_ID和READVIEW的内容
  • 首先比较当前行事务号DB_TRX_ID < up_limit_idid==m_createot_trx_id(满足则看得见), 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断。(该行没有在事务中)
  • 接下来判断 DB_TRX_ID >= low_limit_id(满足也看不见) , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的
【数据库】1、MySQL、事务、MVCC、LBCC_第14张图片
  • 如果被访问的版本的trx_id和ReadView中的creator_trx_id相同,就意味着当前版本就是由你“造成”的,可以读出来。
  • 如果被访问的版本的trx_id小于ReadView中的low_limit_id,表示生成该版本的事务在创建ReadView的时候,已经提交了,所以该版本可以读出来。(在ReadView里的检查过了不符合条件才读不在ReadView里的)
  • 如果被访问版本的trx_id大于或等于ReadView中的up_limit_id值,说明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被读出来。(等于读不出来是因为最大值不是活跃的事务,而是不存在的下个版本)
  • 如果生成被访问版本的trx_id在low_limit_id和up_limit_id之间,那就需要判断下trx_id在不在m_ids中:如果在,说明创建ReadView的时候,生成该版本的事务还是活跃的(没有被提交),该版本不可以被读出来;如果不在,说明创建ReadView的时候,生成该版本的事务已经被提交了,该版本可以被读出来。

如果某个数据的最新版本不可以被读出来,就顺着roll_pointer找到该数据的上一个版本,继续做如上的判断,以此类推,如果第一个版本也不可见的话,代表该数据对当前事务完全不可见,查询结果就不包含这条记录了。

两种隔离级别下的ReadView
①REPEATABLE READ可重复读 隔离级别

关键:只有首次读取数据会创建ReadView(更新事务后会变为未读取状态)

原始版本号0,原始id=1的name为小明

事务1:事务版本号 #1,要修改id=1的name为小明1

事务2:事务版本号#2,要查询id=1的name

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1;

此时事务2生成的READVIEW-2如下:

活跃事务号 row-id name 回滚指针roll_ponit
#1 1 小明1 指向0,该行不在READVIEW中
事务前版本#0 1 小明 本行不在READVIEW中

ReadView-2的其他信息:

  • m_ids是{1},。生成ReadView时,系统中活跃的事务id集合。
  • low_limit_id是1, 。生成ReadView时,系统中活跃的最小事务id,也就是 m_ids中的最小值。
  • up_limit_id是3,。生成ReadView时,系统应该分配给下一个事务的id。
  • creator_trx_id是2。生成该ReadView的事务id。

那么A事务执行的select语句会读到什么数据呢?

  1. 判断最新的数据版本,name是“小明1”,对应的trx_id是#1,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。
  2. 顺着roll_ponit找到这个数据的上一个版本,name是“小明”,对应的trx_id是0,而ReadView中的low_limit_id是1,trx_id

所以读到的数据的name是“小明”。

接着往下执行上面的事务

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1;
commit;
select * from test where id=1; 还是查到小明

随后,事务#1提交了事务,由于REPEATABLE READ是首次读取数据才会创建ReadView,所以事务#2再次执行select语句,不会再创建ReadView,用的还是上一次的ReadView,所以判断流程和上面也是一样的,所以读到的name还是“小明”。

  • 其他知识点:开启事务后,查询id=1后(算是事务开始了),其他会话改了id=2,然后事务1再查id=2,事务1查不到其他会话更改commit后的结果,说明事务1的READVIEW是所有行的
②READ COMMITTED隔离级别

这个隔离级别:每次select读取数据会重新创建ReadView

和上面一样,id=1的对应的原始name为小明,事务1要改为小明1,事务2要查

假设,现在系统只有一个活跃的事务#1,事务id是1,事务中修改了数据,但是还没有提交,形成的版本链是这样的:

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1; 查到的是小明
commit

现在事务2启动,并且执行了select语句,此时会创建出一个ReadView-2,

  • m_ids是{1}
  • up_limit_id是1,
  • low_limit_id是3,
  • creator_trx_id是2。

那么A事务执行的select语句会读到什么数据呢?

  1. 判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。
  2. 顺着roll_pointer找到这个数据的上一个版本,name是“地底王”,对应的trx_id是99,而ReadView中的low_limit_id是100,trx_id

所以读到的数据的name是“地底王”。

事务1提交后事务2再次查

事务1 事务2 说明
begin; begin;
update test set name=‘AA’ where id=1 - 1先改,2再查
select * from test where id=1; 查到小明
commit;
select * from test where id=1; 查到小明1

因为在"读已提交"隔离级别下,每次查会重新创建READVIEW,所以新的参数为:

  • m_ids是{空},
  • up_limit_id是null,
  • low_limit_id是3,
  • creator_trx_id是2。

查的内容不在m_ids里面,说明这个数据版本是由已经提交的事务创建的,该版本可见。所以查到小明1

③④其他隔离级别
  • 对于READ UNCOMMITTED来说,可以读取到其他事务还没有提交的数据,所以直接把这个数据的最新版本读出来就可以了。读未提交是没有加任何锁的,所以对于它来说也就是没有隔离的效果,所以它的性能也是最好的。
  • 对于SERIALIZABLE来说,是用加锁的方式来访问记录。 对于串行化加的是一把大锁,读的时候加共享锁,不能写,写的时候,加的是排它锁,阻塞其它事务的写入和读取,若是其它的事务长时间不能写入就会直接报超时,所以它的性能也是最差的,对于它来就没有什么并发性可言。

④整体流程

我们在了解了隐式字段undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了

整体的流程是怎么样的呢?我们可以模拟一下

  • 事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1事务3在活跃中,事务4事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设我们称为trx_list
事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
修改且已提交
进行中 快照读 进行中
  • Read View不仅仅会通过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1,我更倾向于后者 >>>资料传送门 | 呵呵一笑百媚生的回答) ;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5(这两个命名是反的,真是个坑),trx_list集合的值是1,3,Read View如下图

img

  • 我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。

【数据库】1、MySQL、事务、MVCC、LBCC_第15张图片

  • 所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

【数据库】1、MySQL、事务、MVCC、LBCC_第16张图片

  • 也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

MVCC的术语总结

<高性能MySQL>中对MVCC的部分介绍

  • MySQL的大多数事务型存储引擎实现的其实都不是简单的行级锁。基于提升并发性能的考虑, 它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL, 包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC, 但各自的实现机制不尽相同, 因为MVCC没有一个统一的实现标准。
  • 可以认为MVCC是行级锁的一个变种, 但是它在很多情况下避免了加锁操作, 因此开销更低。虽然实现机制有所不同, 但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
  • MVCC的实现方式有多种,典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。
  • MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

从书中可以了解到:

  • MVCC是被Mysql中 事务型存储引擎InnoDB 所支持的;
  • 应对高并发事务, MVCC比单纯的加锁更高效;
  • MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工作;
  • MVCC可以使用 乐观(optimistic)锁悲观(pessimistic)锁来实现;
  • 各数据库中MVCC实现并不统一

下面的MVCC内容是高性能Mysql中的内容,但我觉得有很多地方跟上面对不上号,觉得他说的是不开启事务的情况的理论,我觉得可以不看。

在MySQL中,会在表中每一条数据后面添加两个字段:

  • 创建版本号:创建一行数据时,将当前系统版本号作为创建版本号赋值
  • 删除版本号:删除一行数据时,将当前系统版本号作为删除版本号赋值

前提:可重复读隔离级别下,MVCC的具体:

SELECT

select时读取数据的规则为:创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号

  • ①创建版本号<=当前事务版本号:保证取出的数据不会有后启动的事务中创建的数据。这也是为什么在开始的示例中我们不会查出后来添加的数据的原因。

  • ②删除版本号为空或>当前事务版本号:保证了在该事物开启之前该数据没有被删除,是应该被查出来的数据。

  • InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是事务开始前已经存在,要么是事务自身插入或修改过的。

  • 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

  • 符合上述两个条件的记录,才能返回作为查询结果。

注意:如果只是执行begin语句实际上并没有开启一个事务。对数据进行了增删改查等操作后才开启了一个事务。

事务 1 事务 2 说明
begin begin
- select * from test
- insert into test(name) values(“E”) 插入表示的是系统版本号
- commit
select * from test 能查看E了。执行增删改查后事务才真正开始,而不是begin时就开始。所以能查到
commit
事务 1 事务 2 说明
begin begin
- select * from test
- insert into test(name) values(“E”) 插入标识的是系统版本号
select * from test 查不到新数据,事务2在事务1查询前还没提交
commit
commit

幻读:

事务 1 事务 2 说明
begin begin 假设现有数据一行A
select * from test
- insert into test(name) values(“B”)
- commit 2提交后1还是只有1条
update test set name=“C” 此时1再查询就已经2条C了
commit

如何解决幻读:

很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,如果我们的项目中需要解决幻读的话也有两个办法:

  • 使用串行化读的隔离级别
  • MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)

实际上很多的项目中是不会使用到上面的两种方法的,串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的。

INSERT

insert时将当前的事务版本号赋值给创建版本号字段。

insert into testmvcc values(1,“test”);

Mysql中MVCC的使用及原理详解

我觉得是隐藏列里的事务版本号变化,隐藏列里并没有create version和delete这一说,所以我觉得网上很多说法并不正确,即上图也不正确,但好理解,实际上事务id只是表现为创建版本号和删除版本号,本质都是只有一个事务id,细节一会挨个讲吧

UPDATE

在更新操作的时候,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。

比如,针对上面那行记录,事务Id为2 要把name字段更新

update table set name= ‘new_value’ where id=1;

Mysql中MVCC的使用及原理详解

修改的时候会先delete后insert。

delete后把行记录里头信息的删除位置为1,然后把版本号写入事务id位置(我觉得细节这么说也不完全正确,下节再说)。

然后再创建一个行,事务id为当前事务id,删除位为空,回滚指针指向刚才删除的那行

DELETE

删除操作的时候,就把删除版本号位填入当前事务版本号。比如

delete from table where id=1;

前面说了delete会把行记录里头信息的删除位置为1,但我又在想,如果删除后回滚呢?怎么还原到原来的事务号?所以我觉得delete操作也会新建一行然后把原来的行标记一下。

MVCC总结

  1. 一般我们认为MVCC有下面几个特点:
    • 每行数据都存在一个版本,每次数据更新时都更新该版本
    • 修改时Copy出当前版本, 然后随意修改,各个事务之间无干扰
    • 保存时比较版本号,如果成功(commit),则覆盖原记录, 失败则放弃copy(rollback)
    • 就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道, 因为这看起来正是,在提交的时候才能知道到底能否提交成功
  2. 而InnoDB实现MVCC的方式是:
    • 事务以排他锁的形式修改原始数据
    • 把修改前的数据存放于undo log,通过回滚指针与主数据关联
    • 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
  3. 二者最本质的区别是: 当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?
  • Innodb的实现真算不上MVCC, 因为并没有实现核心的多版本共存, undo log 中的内容只是串行化的结果, 记录了多个事务的过程, 不属于多版本共存。但理想的MVCC是难以实现的, 当事务仅修改一行记录使用理想的MVCC模式是没有问题的, 可以通过比较版本号进行回滚, 但当事务影响到多行数据时, 理想的MVCC就无能为力了。
  • 比如, 如果事务A执行理想的MVCC, 修改Row1成功, 而修改Row2失败, 此时需要回滚Row1, 但因为Row1没有被锁定, 其数据可能又被事务B所修改, 如果此时回滚Row1的内容,则会破坏事务B的修改结果,导致事务B违反ACID。 这也正是所谓的 第一类更新丢失 的情况。
  • 也正是因为InnoDB使用的MVCC中结合了排他锁, 不是纯的MVCC, 所以第一类更新丢失是不会出现了, 一般说更新丢失都是指第二类丢失更新。

锁机制:

解决因资源共享 而造成的并发问题。
示例:买最后一件衣服X
A: X 买 : X加锁 ->试衣服…下单…付款…打包 ->X解锁
B: X 买 : 发现X已被加锁,等待X解锁, X已售空

分类:
根据操作类型分类:
	a.读锁(共享锁): 对同一个数据(衣服),多个读操作可以同时进行,互不干扰。
	b.写锁(互斥锁): 如果当前写操作没有完毕(买衣服的一系列操作),则无法进行其他的读操作、写操作

操作范围:
	a.表锁 :一次性对一张表整体加锁。如MyISAM存储引擎使用表锁,开销小、加锁快;无死锁;但锁的范围大,容易发生锁冲突、并发度低。
	b.行锁 :一次性对一条数据加锁。如InnoDB存储引擎使用行锁,开销大,加锁慢;容易出现死锁;锁的范围较小,不易发生锁冲突,并发度高(很小概率 发生高并发问题:脏读、幻读、不可重复度、丢失更新等问题)。
	c.页锁	
    
    MyISAM不支持事务,InnoDB支持事务

加锁解锁语法:

-- 增加锁:
lock table 表1  read/ write  ,表2  read/ write   ,...
unlock tables;

-- 查看加锁的表:
show open tables ; #InUse=0代表没有锁

按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。

从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定也防止其他的共享锁定。

为了更改数据,数据库必须在进行更改的行上施加行独占锁定,INSERT、 UPDATE、 DELETE和 SELECT FOR UPDATE语句都会隐式采用必要的行锁定。下面我们介绍一下 Oracle数据库常用的5种锁定。

  • 行共享锁定:一般通过SELECT FOR UPDATE语句隐式获得行共享锁定,在 Oracle中用户也可以通过 LOCK TABLE IN ROW SHARE MODE语句显式获得行共享锁定。行共享锁定并不防止对数据行进行更改的操作,但是可以防止其他会话获取独占性数据表锁定。允许进行多个并发的行共享和行独占性锁定,还允许进行数据表的共享或者采用共享行独占锁定
  • 行独占锁定:通过一条 INSERT、 UPDATE或 DELETE语句隐式获取,或者通过条LOCK TABLE IN ROW EXCLUSMVE MODE语句显式获取。这个锁定可以防止其他会话获取一个共享锁定、共享行独占锁定或独占锁定。
  • 表共享锁定:通过LOCK TABLE IN SHARE MODE语句显式获得。这种锁定可以防止其他会话获取行独占锁定( INSERT、 UPDATE或 DELETB),或者防止其他表共享行独占锁定或表独占锁定,它允许在表中拥有多个行共享和表共享锁定。该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交或者回滚该事务并释放对该表的锁定之前不能更改这个被锁定的表。
  • 表共享行独占:通过 LOCK TABLE IN SHARE ROW EXCLUSTVE MODE语句显式获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,它允许其他行共享锁定。这种锁定类似于表共享锁定,只是一次只能对一个表放置一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行 SELECT FOR UPDATE操作,但如果B会话试图更新选择的行,则需要等待
  • 表独占:通过LOCK TABLE IN EXCLUSIVE MODE显式获得。这个锁定防止其他会话对该表的任何其他锁定。

(1)表锁(MyISAM)

MySQL的默认锁:

MyISAM在执行【查询】语句(SELECT)前,会自动给涉及的所有表加读锁
在执行【增删改】操作(DML)前,会自动给涉及的表加写锁
所以对MyISAM表进行操作,会有以下情况:

  • a、对MyISAM表的读操作(加读锁),不会阻塞其他进程(会话)对同一表的读请求,
    但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
  • b、对MyISAM表的写操作(加写锁),会阻塞其他进程(会话)对同一表的读和写操作,
    只有当写锁释放后,才会执行其它进程的读写操作。
create table tablelock(
	id int primary key auto_increment , 
	name varchar(20)
)engine myisam;#默认InnoDB

表锁定只用于防止其它客户端进行不正当地读取和写入
MyISAM 支持表锁,InnoDB 支持行锁
-- 锁定
    LOCK TABLES tbl_name [AS alias]
-- 解锁
    UNLOCK TABLES

会话:session :每一个访问数据的dos命令行、数据库客户端工具  都是一个会话

myISAM读取时会对需要读到的所有表加共享锁,写入时对表加排它锁。

1.1 表锁读锁(共享锁)

读锁不限制任何对话读,但限制任何对话写

读锁,锁表的当前会话只能读该表,不能写该表,而且不能读/写其他表。其他会话可以读写任何表,但是写该表的时候会等价锁表的会话解锁后才继续执行。

  • – 如果某一个会话 对A表加了read锁,则 该会话 可以对A表进行读操作、不能进行写操作; 且 该会话不能对其他表进行读、写操作。
    – 即如果给A表加了读锁,则当前会话只能对A表进行读操作。

  • 其他会话:a.可以对其他表(A表以外的表)进行读、写操作
    b.对A表:读-可以; 写-需要等待释放锁。

lock table tablelock read

会话1 会话2 会话3 说明
lock table tablelock read;
select * from tablelock; – 读(查),可以
delete from tablelock where id =1 ; – 写(增删改),不可以
select * from emp ; – 不可以读其他表 # 意思是先把读锁去掉再干别的事
delete from emp where eid = 1; – 不可以写其他表
select * from tablelock; – 读(查),可以 select * from emp ; – 读(查),可以
delete from tablelock where id =1 ; – 写,会“等待”会话0将锁释放再执行 delete from emp where eno = 1; – 写,可以

1.2 表锁写锁(排他锁)

lock table table1 write

锁写锁的会话只能写该表,不能增删改查其他表。

其他会话可以任意增删改查,但涉及该表的命令会等待原来会话释放。

分析表锁定:

查看哪些表加了锁:   show open tables ;  In_use=1代表被加了锁

可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定;
show status like 'table%';#分析表锁定的严重程度

	Table_locks_immediate :产生表级锁定的次数,立刻能获取到的锁数,每立即锁值加1
	Table_locks_waited:需要等待的表锁数(如果该值越大,说明存在越大的锁竞争),每等待一次锁值加1
	一般建议:
		Table_locks_immediate/Table_locks_waited > 5000, 建议采用InnoDB引擎,否则MyISAM引擎

区别是一个是锁定的次数,一个是等待的次数

(2)行锁(InnoDB)

create table test(
    id int(5) primary key auto_increment,#主键,有索引
    name varchar(20)
)engine=innodb ;


-- mysql默认自动commit;	oracle默认不会自动commit ;

为了研究行锁,暂时将自动commit关闭:  
set autocommit =0 ; -- 以后记得恢复,不然永久关闭自动提交了
以后需要通过commit

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁

一写一读、相同行:不阻塞,拿到旧数据

事务1 事务2 说明
BEGIN; BEGIN;
update test set name=‘A’ where id=1;
select * from test where id=1;– 2查到旧数据,且没有被1阻塞
select * from test where id=1; – name=A
commit
commit;

两写、相同行:后写的阻塞

事务1 事务2 说明
BEGIN;
update test set name=‘C’ where id=1; id=1被事务1加写锁了
select * from test where id=1;-- name=2
update linelock set name=‘D’ where id=1;-- 阻塞到会话0 commit
commit; 放行

两写:相同行,也阻塞,即使是新插入数据

事务1 事务2 说明
BEGIN; BEGIN
insert into test values(null,‘E’); 此时自增吓一跳是3
update test set name=‘f’ where id = 3; 阻塞
commit; 放行
rollback;

两写、不同行:不会阻塞

事务1 事务2 说明
BEGIN;
update linelock set name=‘2’ where id=1;
select * from linelock where id=1;-- name=2 update linelock set name=‘4’ where id=2;-- 不阻塞不报错
对行锁情况:
	1.如果会话x对某条数据a进行 DML操作(研究时:关闭了自动commit的情况下),则其他会话必须等待会话x结束事务(commit/rollback)后  才能对数据a进行操作。
	2.表锁 是通过unlock tables,也可以通过事务解锁 ; 而行锁 是通过事务解锁。
	
	
行锁的注意事项:
a.如果没有索引,则行锁会转为表锁
show index from linelock ;
alter table linelock add index idx_linelock_name(name);

无索引行锁升级为表锁,类型转换会使索引失效,从而转成行锁

varchar必须加单引号,否则是重罪。不加引号会引起类型转换,造成索引失效,转为表锁

两个会话,id和name都有索引,那么操作不同行,一个where id=一个where name='',不会相互阻塞

两个会话,id和name都有索引,那么操作不同行,一个where id=一个where name=,其中name不加引号
name那个因为类型转换所以可以执行成功,但是因为做了类型转换,那一列索引失效。
此时会话0还没提交,拿id查的那个会话就阻塞了,因为索引失效就转化为表锁了

类型转换造成失效例子:

操作不是索引的列,或索引类 发生了类型转换,则索引失效。 因此 此次操作,会从行锁 转为表锁。

事务1 事务2 说明
begin; begin;
select * from test ; select * from test ;
update test set name = ‘AA’ where name = ‘A’ ;
update test set name = ‘BB’ where name = ‘B’ ; 阻塞
commit 放行
b.行锁的一种特殊情况:间隙锁:值在范围内,但却不存在,也会同样加锁,其他会话插入不了id=7
 -- 此时linelock表中 没有id=7的数据
 update linelock set name ='x' where id >1 and id<9 ;   -- 即在此where范围中,没有id=7的数据,则id=7的数据成为间隙。
间隙:Mysql会自动给间隙加锁 ->加的锁称为间隙锁。即 本题 会自动给id=7的数据加 间隙锁(行锁)。
行锁:如果有where,则实际加索的范围 就是where后面的范围(不是实际的值)

此时另外一个会话要在id=7插入数据,不能插入,需要先在原来update的会话中commit,然后第二个会话就能插入成功了。

有了上面的模拟操作,结果和理论又惊奇的一致,似乎可以放心大胆的实战。。。。。。但现实真的很残酷。

现实:当执行批量修改数据脚本的时候,行锁升级为表锁。其他对订单的操作都处于等待中,,,

原因:==InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁!==而模拟操作正是通过id去作为检索条件,而id又是MySQL自动创建的唯一索引,所以才忽略了行锁变表锁的情况。

所以:给需要作为查询条件的字段添加索引。用完后可以删掉。

总结:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁

从上面的案例看出,行锁变表锁似乎是一个坑,可MySQL没有这么无聊给你挖坑。这是因为MySQL有自己的执行计划。

当你需要更新一张较大表的大部分甚至全表的数据时。而你又傻乎乎地用索引作为检索条件。一不小心开启了行锁(没毛病啊!保证数据的一致性!)。可MySQL却认为大量对一张表使用行锁,会导致事务执行效率低,从而可能造成其他事务长时间锁等待和更多的锁冲突问题,性能严重下降。所以MySQL会将行锁升级为表锁,即实际上并没有使用索引。

我们仔细想想也能理解,既然整张表的大部分数据都要更新数据,在一行一行地加锁效率则更低。其实我们可以通过explain命令查看MySQL的执行计划,你会发现key为null。表明MySQL实际上并没有使用索引,行锁升级为表锁也和上面的结论一致。

行锁分析:

行锁分析:
  show status like '%innodb_row_lock%' ;
	 Innodb_row_lock_current_waits :当前正在等待锁的数量  
	 Innodb_row_lock_time:等待总时长。从系统启到现在 一共等待的时间
	 Innodb_row_lock_time_avg  :平均等待时长。从系统启到现在平均等待的时间
	 Innodb_row_lock_time_max  :最大等待时长。从系统启到现在最大一次等待的时间
	 Innodb_row_lock_waits :	等待次数。从系统启到现在一共等待的次数

加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然我们也可以显示的加锁:

共享锁:select * from tableName where … + lock in share more

排他锁:select * from tableName where … + for update

间隙锁

https://blog.csdn.net/andyxm/article/details/44810417

当id值为1,3,4,5,6…

当我们用范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做”间隙(GAP)”。InnoDB也会对这个”间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

# 此时没有id=2哪一行,
    # 会话0进行
    update linelock set name='3' where id>1 and id<6;
    或
    select ... for update;# 即使没有id=2,也会锁住键值在条件范围内的

    # 会话1:
    insert into linelock values(2,'22');-- 阻塞了,等到会话0 commit才执行

    宁可错杀,不可放过,这个范围的行都锁了,即使没有这个行

间隙锁的主要作用是为了防止出现幻读,但是它会把锁定范围扩大,有时候也会给我们带来麻烦,我们就遇到了。 在数据库参数中, 控制间隙锁的参数是:innodb_locks_unsafe_for_binlog, 这个参数默认值是OFF, 也就是启用间隙锁, 他是一个bool值, 当值为true时表示disable间隙锁。那为了防止间隙锁是不是直接将innodb_locaks_unsafe_for_binlog设置为true就可以了呢? 不一定!而且这个参数会影响到主从复制及灾难恢复, 这个方法还尚待商量。

间隙锁的出现主要集中在同一个事务中先delete 后 insert的情况下, 当我们通过一个参数去删除一条记录的时候,

  • 如果参数在数据库中存在, 那么这个时候产生的是普通行锁, 锁住这个记录, 然后删除, 然后释放锁。
  • 如果这条记录不存在,问题就来了, 数据库会扫描索引,发现这个记录不存在, 这个时候的delete语句获取到的就是一个间隙锁,然后数据库会向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间, 锁住整个区间内的数据, 一个特别容易出现死锁的间隙锁诞生了。
举个例子:
表task_queue
Id           taskId
1              2
3              9
10            20
40            41

开启一个会话: session 1
sql> set autocommit=0;##取消自动提交
sql> delete from task_queue where taskId = 20;
sql> insert into task_queue values(20, 20);

在开启一个会话: session 2
sql> set autocommit=0; ##取消自动提交
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(30, 25);

在没有并发,或是极少并发的情况下, 这样会可能会正常执行,在Mysql中,事务最终都是穿行执行, 但是在高并发的情况下, 执行的顺序就极有可能发生改变, 变成下面这个样子:
sql> delete from task_queue where taskId = 20;
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(20, 20);
sql> insert into task_queue values(30, 25)

这个时候最后一条语句:insert into task_queue values(30, 25); 执行时就会爆出死锁错误。因为删除taskId = 20这条记录的时候,20 --  41 都被锁住了, 他们都取得了这一个数据段的共享锁, 所以在获取这个数据段的排它锁时出现死锁。

这种问题的解决办法:前面说了, 通过修改innodb_locaks_unsafe_for_binlog参数来取消间隙锁从而达到避免这种情况的死锁的方式尚待商量, 那就只有修改代码逻辑, 存在才删除,尽量不去删除不存在的记录。
  • 可重复读 隔离级别:存在间隙锁,可以锁住(2,5)这个间隙,防止其他事务插入数据!
  • 读已提交 隔离级别:不存在间隙锁,其他事务是可以插入数据

比如我们执行下面的语句

update test set color = 'blue' where color = 'red'; 

体现到聚簇索引上为:

读已提交时,会先走聚簇索引,进行全部扫描。加锁如下:

【数据库】1、MySQL、事务、MVCC、LBCC_第17张图片

但在实际中,MySQL做了优化,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁。

读已提交下,实际加锁如下
【数据库】1、MySQL、事务、MVCC、LBCC_第18张图片

然而,在可重复读隔离级别下,走聚簇索引,进行全部扫描,最后会将整个表锁上,如下所示
【数据库】1、MySQL、事务、MVCC、LBCC_第19张图片

如何具体锁定某一行:

select for update;
一读一写,写阻塞

乐观锁

乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了

通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

// 查询出商品库存信息
select number from items where id=1; # number=3
#修改库存为2
update items set number=2 where id=1 and number =3;

以上,我们在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

以上更新语句存在一个比较重要的问题,即传说中的ABA问题

比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中取出库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。改为以下方式即可:

下单操作包括3步骤:
1.查询出商品信息

select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单
3.修改商品status为2

update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

乐观锁每次在执行数据的修改操作时,都会带上一个版本号(或时间戳),一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

以上SQL其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。

有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

# 超卖
update item set number=number-1
where id=1 and number-1 > 0  # 自减之前要确保还有库存  # 因为对该行记录加了些锁,所以相当于是线程同步的

以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制。

以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。

悲观锁

与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。

说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

  1. 乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
  2. 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

排他锁-写锁X

排它锁(Exclusive),又称为X 锁,写锁。

查询该表会锁住该表,禁止其他事务读写

排他锁S锁,也称写锁,独占锁,当前写操作没有完成前,它会阻断其他写锁和读锁

A先查了指定表,B再查该表就阻塞,直到A提交后才执行。

如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

在查询语句后面增加...FOR UPDATE;,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

事务 1 事务 2 说明
begin -
select * from test where id=1 for update -
- select * from test where id=1 没问题
- select * from test where id=1 for update 2阻塞
update test set name=‘F’ where id=1; -
commit; -
- 放行出来结果

【数据库】1、MySQL、事务、MVCC、LBCC_第20张图片

共享锁-读锁S

共享锁(Shared),又称为S 锁,读锁。

查询该表不会造成锁表

读锁多用于判断数据是否存在,多个读操作可以同时进行而不会互相影响。当如果事务对读锁进行修改操作,很可能会造成死锁

读锁只有一种使用方式:SELECT ... LOCK IN SHARE MODE;

在事务当中,读操作SELECT不需要加锁,而是读取undo日志中的最新快照。其中undo日志为用来实现事务回滚,本身没有带来额外的开销

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

在查询语句后面增加,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

【数据库】1、MySQL、事务、MVCC、LBCC_第21张图片

共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴

比如,我这里通过mysql打开两个查询编辑器,在其中开启一个事务,并不执行commit语句
city表DDL如下:

CREATE TABLE `city` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

 

begin;
SELECT * from city where id = "1"    lock in share mode;

然后在另一个查询窗口中,对id为1的数据进行更新

update  city set name="666" where id ="1";

此时,操作界面进入了卡顿状态,过几秒后,也提示错误信息

[SQL]update  city set name="666" where id ="1";
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

那么证明,对于id=1的记录加锁成功了,在上一条记录还没有commit之前,这条id=1的记录被锁住了,只有在上一个事务释放掉锁后才能进行操作,或用共享锁才能对此数据进行操作。
再实验一下:

update city set name="666" where id ="1" lock in share mode;
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1

加上共享锁后,也提示错误信息了,通过查询资料才知道,对于update,insert,delete语句会自动加排它锁的原因

于是,我又试了试SELECT * from city where id = "1" lock in share mode;

你可能感兴趣的:(数据库,mysql,事务,锁)