第三章 事务&锁由浅入深笔记

一、数据库

1、锁的简介

为什么需要锁?

到淘宝上买一件商品,商品只有一件库存,这个时候如果还有另一个人买,那么如何解决是你买到还是另一个人买到的问题?

锁的概念锁是计算机协调多个进程或线程并发访问某一资源的机制。

在数据库中,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。锁对数据库而言显得尤其重要,也更加复杂。

2、MySQL中的锁

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

页面锁(gap锁,间隙锁):开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。在这个部分只讲表级锁、行级锁,gap锁放到事务中讲。

表锁与行锁的使用场景

  • 表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如OLAP系统。
  • 行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适。

3、MyISAM锁

MySQL的表级锁有两种模式:

  • 表共享读锁(Table Read Lock)
  • 表独占写锁(Table Write Lock)

3.1、共享读锁

语法:lock table 表名 read

1)对testmysam表进行加锁(只读)

lock table testmysam READ;

启动另外一个session:

select * from testmysam -- 可以查询

2)插入或者修改数据报错

insert into testmysam value(2);
update testmysam set id=2 where id=1;

3)在另外一个session中插入数据进入等待状态

insert into testmysam value(2);  //等待.....

4)在同一个session中

insert into account value(4,'aa',123);
报错:ERROR 1100 (HY000): Table 'account' was not locked with LOCK TABLES

select * from account ;
报错:ERROR 1100 (HY000): Table 'account' was not locked with LOCK TABLES

5)在另外一个session中给其他表插入数据成功

insert into account value(4,'aa',123);  -- 成功

加索在同一个session中:

select s.* from testmysam s; 
报错:ERROR 1100 (HY000): Table 's' was not locked with LOCK TABLES  

语法:lock table 表名 as 别名 read;

lock table testmysam as t READ;
select t.* from testmysam t;

3.2、独占写锁

1)lock table testmysam WRITE;

在同一个session中

insert testmysam value(3);   //插入成功
delete from testmysam where id = 3; //删除成功
select * from testmysam; //成功

2)对不同的表操作(报错)

select s.* from  testmysam s;
> 1100 - Table 's' was not locked with LOCK TABLES
> 时间: 0s

insert into account value(4,'aa',123);
> 1100 - Table 'account' was not locked with LOCK TABLES
> 时间: 0.001s

3)在其他session中 (等待)

select * from testmysam;

总结:

  • 读锁:对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求。
  • 读锁:对MyISAM表的读操作,不会阻塞当前session对表读,当对表进行修改会报错。
  • 读锁:一个session使用LOCK TABLE命令给表f加了读锁,这个session可以查询锁定表中的记录,但更新或访问其他表都会提示错误。
  • 写锁:对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作。
  • 写锁:对 MyISAM表的写操作,当前session可以对本表做CRUD,但对其他表进行操作会报错。

4、InnoDB锁

在mysql的InnoDB引擎支持行锁

共享锁又称读锁当一个事务对某几行上读锁时,允许其他事务对这几行进行读操作,但不允许其进行写操作,也不允许其他事务给这几行上排它锁,但允许上读锁。

排它锁又称写锁当一个事务对某几个上写锁时,不允许其他事务写,但允许读。更不允许其他事务给这几行上任何锁。包括写锁。

4.1、语法

上共享锁的写法:lock in share mode

例如:select * from 表 where 条件 lock in share mode;

上排它锁的写法:for update

例如:select * from 表 where 条件 for update;

注意:

  • 两个事务不能锁同一个索引。
  • insert ,delete , update在事务中都会自动默认加上排它锁。
  • 行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
CREATE TABLE testdemo (
`id`  int(255) NOT NULL ,
`c1`  varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`c2`  int(50) NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
INDEX `idx_c2` (`c2`) USING BTREE
)
ENGINE=InnoDB;
insert into testdemo VALUES(1,'1',1),(2,'2',2);

示例1:

BEGIN;
select * from testdemo where id =1 for update;
-- 在另外一个session中
update testdemo set c1 = '1' where id = 2;   -- 成功
update testdemo set c1 = '1' where id = 1;  -- 等待

示例2:

BEGIN;
update testdemo set c1 = '1' where id = 1;
-- 在另外一个session中
update testdemo set c1 = '1' where id = 1;  -- 等待

示例3:

BEGIN;
update testdemo set c1 = '1' where  c1 = '1';
-- 在另外一个session中
update testdemo set c1 = '2' where  c1 = '2';  -- 等待

示例4

第一个session中(加上排它锁)

select * from testdemo where id =1 for update; -- 成功

第二个session(加上共享锁)

select * from testdemo where id =1 lock in share mode;   -- 等待

注意:

回到第一个sessionUNLOCK TABLES; 并不会解锁

使用commit或者 begin或者ROLLBACK 才会解锁

示例5再来看下表锁

lock table testdemo WRITE;

使用commit,ROLLBACK 并不会解锁,使用UNLOCK TABLES或者begin会解锁。

4.2、锁的等待问题

 在工作中经常一个数据被锁住,导致另外的操作完全进行不下去。你肯定碰到过这问题,有些程序员在debug程序的时候,经常会锁住一部分数据库的数据,而这个时候你也要调试这部分功能,却发现代码总是运行超时,你是否碰到过这问题了,其实这问题的根源我相信你也知道了。

举例来说,有两个会话。

程序员甲,正直调试代码:

BEGIN;
SELECT * FROM testdemo WHERE id = 1 FOR UPDATE;

你正直完成的功能也要经过那部分的代码,你得上个读锁

另外一个session执行下面代码:

BEGIN;
SELECT * FROM testdemo WHERE id = 1 lock in share mode;

这个时候很不幸,你并不知道发生了什么问题,在你调试得过程中永远就是一个超时得异常,而这种问题不管在开发中还是在实际项目运行中都可能会碰到,那么怎么排查这个问题呢?

这其实也是有小技巧的。

select * from information_schema.INNODB_LOCKS;

我通过这个sql语句起码发现在同一张表里面得同一个数据有了2个锁其中一个是X(写锁),另外一个是S(读锁),我可以跳过这一条数据,使用其他数据做调试。

可能如果我就是绕不过,一定就是要用这条数据呢?吼一嗓子吧(哪个缺德的在debug这个表,请不要锁着不动),其实还有更好的方式来看。

select * from sys.innodb_lock_waits;

执行命令:KILL 5;

> 1317 - Query execution was interrupted

> 时间: 0s

我现在执行的这个sql语句有了,另外看下最下面,kill命令,你在工作中完全可以通过kill把阻塞了的sql语句给干掉,你就可以继续运行了,不过这命令也要注意使用,如果某同事正在做比较重要的调试,你kill,被他发现可能会被暴打一顿。

上面的解决方案不错,但如果你的MySQL不是5.7的版本呢?是5.6呢,你根本就没有sys库,这个时候就难办了,不过也是有办法的。

1)同理在本地MySQL5.6里面执行下面查询:

BEGIN;
SELECT * FROM t_user WHERE id = 1 FOR UPDATE;

2)然后在另外一个session里面执行语句:

BEGIN;
SELECT * FROM t_user WHERE id = 1 lock in share mode;

3)最后在执行下面的锁查询语句:

SELECT r.trx_id waiting_trx_id, r.trx_mysql_thread_id waiting_thread,
  r.trx_query waiting_query,  b.trx_id blocking_trx_id,b.trx_mysql_thread_id blocking_thread
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

看到没有,接下来你是否也可以执行kill 29 这样的大招了。

kill 3
> 1317 - Query execution was interrupted
> 时间: 0s

再次查询发现锁已经不存在了。

二、数据库事务

1、为什么需要事务

现在的很多软件都是多用户,多程序,多线程的,对同一个表可能同时有很多人在用,为保持数据的一致性,所以提出了事务的概念。

A 给B 要划钱,A 的账户-1000元, B 的账户就要+1000元,这两个update 语句必须作为一个整体来执行,不然A 扣钱了,B 没有加钱这种情况很难处理。

2、什么存储引擎支持事务

1.查看数据库下面是否支持事务(InnoDB支持)?

show engines;

2.查看mysql当前默认的存储引擎?

show variables like '%storage_engine%';

2.3、查看某张表的存储引擎?

show create table 表名 ;

2.4、对于表的存储结构的修改?

建立InnoDB 表:Create table .... type=InnoDB; Alter table table_name type=InnoDB;

3、事务特性

事务应该具有4个属性:

原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability),这四个属性通常称为ACID特性。

3.1、原子性(atomicity)

一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不可能只执行其中的一部分操作,整个事务要么全部成功,要么全部失败。

3.2、一致性(consistency)

一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。

3.3、持久性(durability)

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失。并不是数据库的角度完全能解决。

3.4、隔离性(isolation)

一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰(对数据库的并行执行,应该像串行执行一样)。

  • 未提交读(READ UNCOMMITED)脏读
  • 已提交读 (READ COMMITED)不可重复读
  • 可重复读(REPEATABLE READ)
  • 可串行化(SERIALIZABLE)

mysql默认的事务隔离级别为repeatable-read

show variables like '%tx_isolation%';

3.5、事务并发问题

脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。

不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

3.6、未提交读(READ UNCOMMITED)脏读

show variables like '%tx_isolation%';
set SESSION TRANSACTION ISOLATION LEVEL read UNCOMMITTED;

一个session中

start TRANSACTION;
update account set balance = balance -50 where id = 1;

另外一个session中查询

select * from account;

回到第一个session中 回滚事务

ROLLBACK;

在第二个session中

select * from account;

在另外一个session中读取到了为提交的数据,这部分的数据为脏数据。

3.7、已提交读 (READ COMMITED)不可重复读

show variables like '%tx_isolation%';
set SESSION TRANSACTION ISOLATION LEVEL read committed;

一个session中

start TRANSACTION;
update account set balance = balance -50 where id = 1;

另外一个session中查询 (数据并没改变)

select * from account;

回到第一个session中 回滚事务

Commit;

在第二个session中

select * from account (数据已经改变)

3.8、可重复读(REPEATABLE READ)

一个session中

start TRANSACTION;
update account set balance = balance -50 where id = 1;

另外一个session中查询 (数据并没改变)

select * from account;

回到第一个session中 回滚事务

Commit;

在第二个session中

select * from account (数据并未改变)

3.9、可串行化(SERIALIZABLE)

1)开启一个事务

Begin;
select * from account;  -- 发现3条记录

2)开启另外一个事务

Begin;
select * from account;  -- 发现3条记录 也是3条记录
insert into account VALUES(4,'deer',500)  -- 发现根本就不让插入

回到第一个事务 commit; 以后插入成功。

 

 

猜你喜欢

转载自blog.csdn.net/m0_37661458/article/details/93328767