diff --git "a/44\344\273\243\347\221\236/20231019\344\272\213\345\212\241.md" "b/44\344\273\243\347\221\236/20231019\344\272\213\345\212\241.md" new file mode 100644 index 0000000000000000000000000000000000000000..dc6aadf18f67e634db9189b379f8931158809f26 --- /dev/null +++ "b/44\344\273\243\347\221\236/20231019\344\272\213\345\212\241.md" @@ -0,0 +1,789 @@ +事务详解 +1、本篇内容 +什么是事务,它有什么用? +事务的几个特性 +事务常见操作指令详解 +事务的隔离级别详解 +脏读、不可重复读、可重复读、幻读详解 +演示各种隔离级别产生的现象 +关于隔离级别的选择 +2、什么是事务? +数据库中的事务是指对数据库执行一批操作,在同一个事务当中,这些操作最终要么全部执行成功,要么全部失败,不会存在部分成功的情况。 + +事务是一个原子操作。是一个最小执行单元。可以甶一个或多个SQL语句组成 +在同一个事务当中,所有的SQL语句都成功执行时,整 个事务成功,有一个SQL语句执行失败,整个事务都执行失败。 +举个例子: + +比如A用户给B用户转账100操作,过程如下: + +从A账户扣100 +给B账户加100 +如果在事务的支持下,上面最终只有2种结果: + +操作成功:A账户减少100;B账户增加100 +操作失败:A、B两个账户都没有发生变化 +如果没有事务的支持,可能出现错:A账户减少了100,此时系统挂了,导致B账户没有加上100,而A账户凭空少了100。 + +3、事务的几个特性(ACID) -重点 +原子性(Atomicity) +事务的整个过程如原子操作一样,最终要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。 + +一致性(Consistency) +一个事务必须使数据库从一个一致性状态变换到另一个一致性状态。 + +首先回顾一下一致性的定义。所谓一致性,指的是数据处于一种有意义的状态,这种状态是语义上的而不是语法上的。最常见的例子是转帐。例如从帐户A转一笔钱到帐户B上,如果帐户A上的钱减少了,而帐户B上的钱却没有增加,那么我们认为此时数据处于不一致的状态。 + +从这段话的理解来看,所谓一致性,即,从实际的业务逻辑上来说,最终结果是对的、是跟程序员的所期望的结果完全符合的 + +隔离性(Isolation) +一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 + +这里先提一下事务的隔离级别: +读未提交:read uncommitted +读已提交:read committed +可重复读:repeatable read +串行化:serializable +持久性(Durability) +一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘,修改是永久性的。 + +4、Mysql中事务操作 +mysql中事务默认是隐式事务,执行insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。 + +是否开启隐式事务是由变量autocommit控制的。 + +所以事务分为隐式事务和显式事务。 + +隐式事务 +事务自动开启、提交或回滚,比如insert、update、delete语句,事务的开启、提交或回滚由mysql内部自动控制的。 + +查看变量autocommit是否开启了自动提交 + +mysql> show variables like 'autocommit'; ++---------------+-------+ +| Variable_name | Value | ++---------------+-------+ +| autocommit | ON | ++---------------+-------+ +1 row in set, 1 warning (0.00 sec) +1 +2 +3 +4 +5 +6 +7 +autocommit为ON表示开启了自动提交。 + +显式事务 +事务需要手动开启、提交或回滚,由开发者自己控制。 + +2种方式手动控制事务: + +方式1: +语法: + +//设置不自动提交事务 +set autocommit=0; +//执行事务操作 +commit|rollback; +1 +2 +3 +4 +示例1:提交事务操作,如下: + +mysql> create table test1 (a int); +Query OK, 0 rows affected (0.01 sec) + +mysql> select * from test1; +Empty set (0.00 sec) + +mysql> set autocommit=0; +Query OK, 0 rows affected (0.00 sec) + +mysql> insert into test1 values(1); +Query OK, 1 row affected (0.00 sec) + +mysql> commit; +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | ++------+ +1 row in set (0.00 sec) + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +示例2:回滚事务操作,如下: + +mysql> set autocommit=0; +Query OK, 0 rows affected (0.00 sec) + +mysql> insert into test1 values(2); +Query OK, 1 row affected (0.00 sec) + +mysql> rollback; +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | ++------+ +1 row in set (0.00 sec) + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +可以看到上面数据回滚了。 + +我们把autocommit还原回去: + +mysql> set autocommit=1; +Query OK, 0 rows affected (0.00 sec) +1 +2 +方式2: +语法: + +start transaction;//开启事务 +//执行事务操作 +commit|rollback; +1 +2 +3 +示例1:提交事务操作,如下: + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | ++------+ +1 row in set (0.00 sec) + +mysql> start transaction; +Query OK, 0 rows affected (0.00 sec) + +mysql> insert into test1 values (2); +Query OK, 1 row affected (0.00 sec) + +mysql> insert into test1 values (3); +Query OK, 1 row affected (0.00 sec) + +mysql> commit; +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | +| 2 | +| 3 | ++------+ +3 rows in set (0.00 sec) + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +上面成功插入了2条数据。 + +示例2:回滚事务操作,如下: + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | +| 2 | +| 3 | ++------+ +3 rows in set (0.00 sec) + +mysql> start transaction; +Query OK, 0 rows affected (0.00 sec) + +mysql> delete from test1; +Query OK, 3 rows affected (0.00 sec) + +mysql> rollback; +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | +| 2 | +| 3 | ++------+ +3 rows in set (0.00 sec) + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +上面事务中我们删除了test1的数据,显示删除了3行,最后回滚了事务。 + +5、savepoint关键字 +在事务中我们执行了一大批操作,可能我们只想回滚部分数据,怎么做呢? + +我们可以将一大批操作分为几个部分,然后指定回滚某个部分。可以使用savepoin来实现,效果如下: + +先清除test1表数据: + +mysql> delete from test1; +Query OK, 3 rows affected (0.00 sec) + +mysql> select * from test1; +Empty set (0.00 sec) +1 +2 +3 +4 +5 +演示savepoint效果,认真看: + +mysql> start transaction; +Query OK, 0 rows affected (0.00 sec) + +mysql> insert into test1 values (1); +Query OK, 1 row affected (0.00 sec) + +mysql> savepoint part1;//设置一个保存点 +Query OK, 0 rows affected (0.00 sec) + +mysql> insert into test1 values (2); +Query OK, 1 row affected (0.00 sec) + +mysql> rollback to part1;//将savepint = part1的语句到当前语句之间所有的操作回滚 +Query OK, 0 rows affected (0.00 sec) + +mysql> commit;//提交事务 +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | ++------+ +1 row in set (0.00 sec) + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +从上面可以看出,执行了2次插入操作,最后只插入了1条数据。 + +savepoint需要结合rollback to sp1一起使用,可以将保存点sp1到rollback to之间的操作回滚掉。 + +6、 只读事务 +表示在事务中执行的是一些只读操作,如查询,但是不会做insert、update、delete操作,数据库内部对只读事务可能会有一些性能上的优化。 + +用法如下: + +start transaction read only; +1 +示例: + +mysql> commit; +Query OK, 0 rows affected (0.00 sec) + +mysql> start transaction read only; +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | +| 1 | ++------+ +2 rows in set (0.00 sec) + +mysql> delete from test1; +ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction. +mysql> commit; +Query OK, 0 rows affected (0.00 sec) + +mysql> select * from test1; ++------+ +| a | ++------+ +| 1 | +| 1 | ++------+ +2 rows in set (0.00 sec) + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +只读事务中执行delete会报错。 + +7、事务中的一些问题(重点) +这些问题主要是基于数据在多个事务中的可见性来说的。也是并发事务产生的问题。 + +更新丢失 +丢失更新就是两个不同的事务(或者Java程序线程)在某一时刻对同一数据进行读取后,先后进行修改。导致第一次操作数据丢失。 + +「 + +第一类丢失更新 :A,B 事务同时操作同一数据,A先对改数据进行了更改,B再次更改时失败然后回滚,把A更新的数据也回滚了。(事务撤销造成的撤销丢失) + +第二类丢失更新:A,B 事务同时操作同一数据,A先对改数据进行了更改,B再次更改并且提交,把A提交的数据给覆盖了。(事务提交造成的覆盖丢失) + +」 + +脏读 +一个事务在执行的过程中读取到了其他事务还没有提交的数据。 这个还是比较好理解的。 + +「 + +两个事务同时操作同一数据,A事务对该数据进行了修改还没提交的时候,B事务访问了该条事务,并且使用了该数据,此时A事务回滚,那么B事务读到的就是脏数据。 + +比如事务1,修改了某个数据 事务2,刚好访问了事务1修改后的数据 + +此时事务1,回滚了操作 事务2,读到还是回滚前的数据 + +」 + +读已提交 +从字面上我们就可以理解,即一个事务操作过程中可以读取到其他事务已经提交的数据。 + +事务中的每次读取操作,读取到的都是数据库中其他事务已提交的最新的数据(相当于当前读) + +不可重复读 +在同一事务中,多次读取同一数据返回的结果有所不同,换句话说,后续读取可以读到另一事务已提交的更新数据。相反,“可重复读” 在同一事务中多次读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据。 + +「 + +这种情况发生 在一个事务内多次读同一数据。A事务查询某条数据,该事务未结束时,B事务也访问同一数据并进行了修改。那么在A事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。 + +事务1,查询某个数据 事务2,修改了某个数据,提交 + +事务1,再次查询这个数据 + +这样事务1两次查询的数据不一样,称为不可重复读 + +」 + +可重复读 +一个事务操作中对于一个读取操作不管多少次,读取到的结果都是一样的。 + +幻读 +脏读、不可重复读、可重复读、幻读,其中最难理解的是幻读 + +以mysql为例: + +幻读现象例子: + +可重复读模式下,比如有个用户表,手机号码为主键,有两个事物进行如下操作 +事务A操作如下: 1、打开事务 2、查询号码为X的记录,不存在 3、插入号码为X的数据,插入报错(为什么会报错,先向下看) 4、查询号码为X的记录,发现还是不存在(由于是可重复读,所以读取记录X还是不存在的) +事物B操作:在事务A第2步操作时插入了一条X的记录,所以会导致A中第3步插入报错(违反了唯一约束) +上面操作对A来说就像发生了幻觉一样,明明查询X(A中第二步、第四步)不存在,但却无法插入成功 +幻读可以这么理解:事务中后面的操作(插入号码X)需要上面的读取操作(查询号码X的记录)提供支持,但读取操作却不能支持下面的操作时产生的错误,就像发生了幻觉一样。 +看第二种解释: + +事务A在操作一堆数据的时候,事务B插入了一条数据,A事务再次(第二次)查询,发现多了一条数据,像是幻觉。与不可重复读类似,不同的是一个是修改删除操作,一个是新增操作。 +如果还是理解不了的,继续向下看,后面后详细的演示。 + +8、事务的隔离级别 +当多个事务同时进行的时候,如何确保当前事务中数据的正确性,比如A、B两个事物同时进行的时候,A是否可以看到B已提交的数据或者B未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。 + +事务隔离级别主要是解决了上面多个事务之间数据可见性及数据正确性的问题。(或者说为了解决并发控制可能产生的异常问题,数据库定义了四种事务的隔离级别) + +隔离级别分为4种: + +读未提交:READ-UNCOMMITTED +读已提交:READ-COMMITTED +可重复读:REPEATABLE-READ +串行:SERIALIZABLE +上面4中隔离级别越来越强,会导致数据库的并发性也越来越低。 + +查看隔离级别 +mysql> show variables like 'transaction_isolation'; ++-----------------------+----------------+ +| Variable_name | Value | ++-----------------------+----------------+ +| transaction_isolation | READ-COMMITTED | ++-----------------------+----------------+ +1 row in set, 1 warning (0.00 sec) +1 +2 +3 +4 +5 +6 +7 +隔离级别的设置 +分2步骤,修改文件、重启mysql,如下: + +修改mysql中的my.init文件,我们将隔离级别设置为:READ-UNCOMMITTED,如下: + +# 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行 + +transaction-isolation=READ-UNCOMMITTED +1 +2 +3 +以管理员身份打开cmd窗口,重启mysql,如下: + +C:\Windows\system32>net stop mysql +mysql 服务正在停止.. +mysql 服务已成功停止。 + +C:\Windows\system32>net start mysql +mysql 服务正在启动 . +mysql 服务已经启动成功。 +1 +2 +3 +4 +5 +6 +7 +9、各种隔离级别中会出现的问题 +隔离级别 脏读可能性 不可重复读可能性 幻读可能性 +READ-UNCOMMITTED 有 有 有 +READ-COMMITTED 无 有 有 +REPEATABLE-READ 无 无 有 +SERIALIZABLE 无 无 无 +下面我们来演示一下,各种隔离级别中可见性的问题,开启两个窗口,叫做A、B窗口,两个窗口中登录mysql。 + +9.1、READ-UNCOMMITTED:读未提交作业 + +```MySQL +-- 部门表 +create table dept( + deptno int primary key auto_increment, -- 部门编号 + dname varchar(14) , -- 部门名字 + loc varchar(13) -- 地址 +) ; +-- 员工表 +create table emp( + empno int primary key auto_increment,-- 员工编号 + ename varchar(10), -- 员工姓名 - + job varchar(9), -- 岗位 + mgr int, -- 直接领导编号 + hiredate date, -- 雇佣日期,入职日期 + sal int, -- 薪水 + comm int, -- 提成 + deptno int not null, -- 部门编号 + foreign key (deptno) references dept(deptno) +); +insert into dept values(10,'财务部','北京'); +insert into dept values(20,'研发部','上海'); +insert into dept values(30,'销售部','广州'); +insert into dept values(40,'行政部','深圳'); +insert into emp values(7369,'刘一','职员',7902,'1980-12-17',800,null,20); +insert into emp values(7499,'陈二','推销员',7698,'1981-02-20',1600,300,30); +insert into emp values(7521,'张三','推销员',7698,'1981-02-22',1250,500,30); +insert into emp values(7566,'李四','经理',7839,'1981-04-02',2975,null,20); +insert into emp values(7654,'王五','推销员',7698,'1981-09-28',1250,1400,30); +insert into emp values(7698,'赵六','经理',7839,'1981-05-01',2850,null,30); +insert into emp values(7782,'孙七','经理',7839,'1981-06-09',2450,null,10); +insert into emp values(7788,'周八','分析师',7566,'1987-06-13',3000,null,20); +insert into emp values(7839,'吴九','总裁',null,'1981-11-17',5000,null,10); +insert into emp values(7844,'郑十','推销员',7698,'1981-09-08',1500,0,30); +insert into emp values(7876,'郭十一','职员',7788,'1987-06-13',1100,null,20); +insert into emp values(7900,'钱多多','职员',7698,'1981-12-03',950,null,30); +insert into emp values(7902,'大锦鲤','分析师',7566,'1981-12-03',3000,null,20); +insert into emp values(7934,'木有钱','职员',7782,'1983-01-23',1300,null,10); +-- 完成以下练习题 +-- +-- 1、列出最低薪金大于1500的各种工作。 +-- +select job,sal from emp where sal>1500; +-- 2、列出在部门 "销售部" 工作的员工的姓名,假定不知道销售部的部门编号。 +-- +select * from emp; + +select d.dname,ename from emp e,dept d where e.deptno=d.deptno and dname='销售部'; +-- 3、列出薪金高于公司平均薪金的所有员工。 +-- +select * from emp; + +with +a as (select avg(sal) over() as n from emp) +select distinct ename,sal,a.n from emp e,a where e.sal>n; +-- 4、列出与"周八"从事相同工作的所有员工。 +-- +select * from emp; + +with +a as (select job from emp where ename='周八') +select * from emp e,a where e.job=a.job; +-- 5、列出薪金等于部门30中员工的薪金的所有员工的姓名和薪金。 +-- +select * from emp; + +with +a as (select sal from emp where deptno=30) +select ename,e.sal from emp e,a where e.sal=a.sal; +-- 6、列出薪金高于在部门30工作的所有员工的薪金的员工姓名和薪金。 +-- +select * from emp; + +with +a as (select max(sal) over() n from emp where deptno=30) +select distinct ename,e.sal from emp e,a where e.sal>a.n; +-- 7、列出在每个部门工作的员工数量、平均工资、平均服务年限。 +-- +select * from emp; + +with +a as (select distinct count(*) over(partition by dname) as a1,dname from emp e,dept d where e.deptno=d.deptno), +b as (select distinct avg(sal) over(partition by dname) as b1,dname from emp e,dept d where e.deptno=d.deptno), +c as (select distinct avg(floor(datediff(now(),hiredate)/365)) over(partition by dname) as c1,dname from emp e,dept d where e.deptno=d.deptno) +select a.a1 员工数量,b.b1 平均工资,c.c1 平均服务年限,d.dname 部门 from a,b,c,dept d where a.dname=b.dname and b.dname=c.dname and c.dname=d.dname; +-- 8、列出所有员工的姓名、部门名称和工资。 +-- +select * from emp; + +-- 部门表 +create table dept( + deptno int primary key auto_increment, -- 部门编号 + dname varchar(14) , -- 部门名字 + loc varchar(13) -- 地址 +) ; +-- 员工表 +create table emp( + empno int primary key auto_increment,-- 员工编号 + ename varchar(10), -- 员工姓名 - + job varchar(9), -- 岗位 + mgr int, -- 直接领导编号 + hiredate date, -- 雇佣日期,入职日期 + sal int, -- 薪水 + comm int, -- 提成 + deptno int not null, -- 部门编号 + foreign key (deptno) references dept(deptno) +); +insert into dept values(10,'财务部','北京'); +insert into dept values(20,'研发部','上海'); +insert into dept values(30,'销售部','广州'); +insert into dept values(40,'行政部','深圳'); +insert into emp values(7369,'刘一','职员',7902,'1980-12-17',800,null,20); +insert into emp values(7499,'陈二','推销员',7698,'1981-02-20',1600,300,30); +insert into emp values(7521,'张三','推销员',7698,'1981-02-22',1250,500,30); +insert into emp values(7566,'李四','经理',7839,'1981-04-02',2975,null,20); +insert into emp values(7654,'王五','推销员',7698,'1981-09-28',1250,1400,30); +insert into emp values(7698,'赵六','经理',7839,'1981-05-01',2850,null,30); +insert into emp values(7782,'孙七','经理',7839,'1981-06-09',2450,null,10); +insert into emp values(7788,'周八','分析师',7566,'1987-06-13',3000,null,20); +insert into emp values(7839,'吴九','总裁',null,'1981-11-17',5000,null,10); +insert into emp values(7844,'郑十','推销员',7698,'1981-09-08',1500,0,30); +insert into emp values(7876,'郭十一','职员',7788,'1987-06-13',1100,null,20); +insert into emp values(7900,'钱多多','职员',7698,'1981-12-03',950,null,30); +insert into emp values(7902,'大锦鲤','分析师',7566,'1981-12-03',3000,null,20); +insert into emp values(7934,'木有钱','职员',7782,'1983-01-23',1300,null,10); +-- 完成以下练习题 +-- +-- 1、列出最低薪金大于1500的各种工作。 +-- +select job,sal from emp where sal>1500; +-- 2、列出在部门 "销售部" 工作的员工的姓名,假定不知道销售部的部门编号。 +-- +select * from emp; + +select d.dname,ename from emp e,dept d where e.deptno=d.deptno and dname='销售部'; +-- 3、列出薪金高于公司平均薪金的所有员工。 +-- +select * from emp; + +with +a as (select avg(sal) over() as n from emp) +select distinct ename,sal,a.n from emp e,a where e.sal>n; +-- 4、列出与"周八"从事相同工作的所有员工。 +-- +select * from emp; + +with +a as (select job from emp where ename='周八') +select * from emp e,a where e.job=a.job; +-- 5、列出薪金等于部门30中员工的薪金的所有员工的姓名和薪金。 +-- +select * from emp; + +with +a as (select sal from emp where deptno=30) +select ename,e.sal from emp e,a where e.sal=a.sal; +-- 6、列出薪金高于在部门30工作的所有员工的薪金的员工姓名和薪金。 +-- +select * from emp; + +with +a as (select max(sal) over() n from emp where deptno=30) +select distinct ename,e.sal from emp e,a where e.sal>a.n; +-- 7、列出在每个部门工作的员工数量、平均工资、平均服务年限。 +-- +select * from emp; + +with +a as (select distinct count(*) over(partition by dname) as a1,dname from emp e,dept d where e.deptno=d.deptno), +b as (select distinct avg(sal) over(partition by dname) as b1,dname from emp e,dept d where e.deptno=d.deptno), +c as (select distinct avg(floor(datediff(now(),hiredate)/365)) over(partition by dname) as c1,dname from emp e,dept d where e.deptno=d.deptno) +select a.a1 员工数量,b.b1 平均工资,c.c1 平均服务年限,d.dname 部门 from a,b,c,dept d where a.dname=b.dname and b.dname=c.dname and c.dname=d.dname; +-- 8、列出所有员工的姓名、部门名称和工资。 +-- +select * from emp; + +select ename,dname,sal from emp e,dept d where e.deptno=d.deptno; +-- 9、列出所有部门的详细信息和部门人数。 +-- +select * from emp; + +with +a as (select distinct count(*) over(partition by dname) as a1,dname from emp e,dept d where e.deptno=d.deptno) +select d.*,a.a1 部门人数 from dept d,a where d.dname=a.dname; +-- 10、列出各种工作的最低工资。 +-- +select * from emp; + +select distinct job,min(sal) over(partition by job) from emp; +-- 11、列出各个部门的 经理 的最低薪金。 +-- +select * from emp; + +select dname,min(sal) over(partition by e.deptno) from emp e,dept d where e.deptno=d.deptno and job='经理'; +-- 12、列出所有员工的年工资,按年薪从低到高排序。 +-- +select * from emp; + + +select *,(sal+ifnull(comm,0))*12 as 年工资 from emp order by 年工资; +``` \ No newline at end of file