编程爱好者之家

mysql 事务简介

2018-02-28 19:24:27 287

事物(基于 innodb 引擎的数据库,对于 myisam 引擎数据库就不支持事务)

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

         例如:A-B 转账,对应的如下 sql 语句

                    update from account set money=money-100 where name='a';

                    update from account set money=money+100 where name='b';

                    数据库默认事务是自动提交的, 也就是发一条 sql 它就执行一条。如果想多条 sql 放在一个事务中执行,则需要使用事务进行处理。当我们开启一个事务,并且没有提交,mysql 会自动回滚事务。或者我们使用 rollback 命令手动回滚事务。

 

         2、数据库开启事务命令:

               start transcation 开启事务

               rollback 回滚事务

               commit 提交事务

        上面这个实验是 a 向 b 转账 100 元,那么就有两条更新的 sql 语句需要执行,所以我们需要将这两个 sql 作为一个事务进行操作,要么更新都成功,要么更新都失败。在开始的时候我们将 mysql 的自动提交关闭。connnectino.setAutoCommit(false); 我们将手动控制事务的提交行为。在执行更新操作的时候有可能出现意外情况,导致交易终止,所以我们将捕获可能出现的异常,在处理异常的时候为了保证数据库的一致性,我们必须将上面的事务操作进行回滚,取消双方的交易。connectino.rollback();  如果没有意外情况,那么程序继续执行,最后我们将事务一起提交。connection.commit(); 现在假设有这样一种情况,即使中间出现了意外情况,那么我们也不想将事务中的所有 sql 语句都回滚,我们可以在想要保留的操作后面增加一个还原点。connection.setSavepoint(),用一个 Savepoint 对象接收,在 异常情况出现以后,我们进行回滚操作的时候将回滚到我们的还原点上。然后提交数据。connection.rollback(savepoin); 这样就形成了还原点以前的更新操作可以执行。注意,我们通常在 rollback 以后进行一下 commit 操作,这样是为了避免一些想要执行的操作丢失的问题。

 

二、事物的四大特性 (ACID

       1、原子性(Atomicity

               事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

          2、一致性(Consistency

               事务前后数据的完整性必须保持一致。

          3、隔离性(Isolation

                多个用户并发访问数据库时,一个用户的事务不能被其它用户的事物所干扰,多个并发事务之间的数据要相互隔离。

          4、持久性(Durability

                一个事务一旦被提交,它对数据库中的数据改变就是永久性的。

 

          事务的隔离级别:

          多个线程开启各自的事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。如果事务不考虑隔离性会引发以下问题:

          (1)脏读:

                   指一个事务读取了另外一个事务未提交的数据。比如 A 向 B 购买商品,如果 B 的事务隔离级别为最低的 read uncommitted,那么当 A 执行了 update account set money=money+100 where name='B';以后并没有提交数据的时候,B 进行了 select money from account where name='B';查询账户的操作,由于 B 的事物隔离级别最低,所以导致了脏读,读取到了 A 没有提交的数据,当 A 执行了 rollback 回滚命令以后,B 再查询账户,就发现先前增加的 100 元消失了。为了避免脏读,我们可以将事务的隔离级别设置为:read committed。

          (2)不可重复读:

                   在一个事务内读取到了表中的某一行数据,多次读取结果不同。不可重复读和脏读的区别是:脏读是读取前一事务未提交的数据,不可重复读是重新读取了前一个事务已提交的数据。比如还是刚才的情景,当 B 将自己的事务隔离级别设置了 read committed 时,可以避免脏读,也就是别人没有提交的数据是读不到的。但是如果 A 将数据提交了,执行了 commit 命令后,B 在这个当前事务内再次查询账户的时候,就发现账户多了 100 元,这种情况看似是符合逻辑的,但是我们这里说到的不可重复读是指在这个当前事务内,不可以发生两次读取操作结果不一致的可能性,我们要保证在一个事务中,我们多次从数据库获取的数据应该是一致的,这样才能保证我们进行数据操作的可靠性。为了避免这个为题,我们可以将数据库的事务隔离级别设置为:repeatable read,这样就保证了在一个事务中,每次读取到数据都是一致的。

          (3)虚读 ( 幻读 )

                     在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。和不可重复读的区别是:不可重复读是读取到了别人对表中的某一条记录进行了修改,导致前后读取的数据不一致。  虚读是前后读取到表中的记录总数不一样,读取到了其它事务插入的数据。比如现在有 A 和 B 两个应用程序,他们并发访问了数据库中的某一张表,假设表中有 3 条记录,B 执行查询操作, 第一次查询表得到了 3 条记录。此时 A 对表进行了修改,增加了一条记录,当 B 再次查询表的时候,发现多了一条数据。这种情况就造成了 B 的虚读。但是虚读是不一定每次都发生的,这种情况是不确定的。为了避免虚读,我们可以将事物隔离级别设置为 serializable 如果设置成了这种级别,那么数据库就变成了单线程访问的数据库,导致性能降低很多。

 

             为了解决以上没有考虑三个隔离性可能引发的问题,数据库定义了四种隔离级别:set transaction isolation level (设置事务隔离级别) select @@tx_isolation (查询当前事务隔离级别)

 

             (1)Serializable:可避免脏读、不可重复读、虚读情况的发生。             

             (2)Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读,是 mysql 默认的事务隔离级别)

             (3)Read committed:可避免脏读情况发生。(读取已提交的数据)

             (4)Read uncommitted:最低级别,以上情况均无法保证。(读取到了未提交的数据)

 

               当我们将数据库的隔离级别设置为:Serializable 的时候,虽然可以避免所有并发访问的问题,但是 Serializable 采用的是单线程来解决并发访问的问题,也就是说在某一段时间内,只能有一个用户对数据库进行操作,导致其它用户阻塞。导致数据库的访问性能很差。

 

三、锁 (共享锁、排它锁、悲观锁、乐观锁、行级锁、表级锁)

         1、共享锁

         如果当前事务隔离级别是最高级别:serializable,那么当做查询操作时数据库会为程序分配一个共享锁。共享锁和共享锁之间是不冲突的,如果一个程序加了共享锁,另外一个程序数据库也可以为其分配一个共享锁。特点:共享锁和共享锁之间不冲突,共享锁和排它锁是冲突的。

         2、排它锁

         如果对数据库进行更新操作,数据库会为它的每个记录加一把排它锁,假如现在 A 程序的事务隔离级别为 serializable,A 对数据库进行查询操作,也就是数据库为表中的记录增加了一个共享锁,当 B 程序并发访问数据库时,B 对数据库进行更新操作,此时数据库就为表中的每个记录增加了一个排它锁,由于共享锁和排它锁是冲突的,数据库就无法为表记录再加排它锁,所以导致了 B 程序的阻塞。这也就是为什么隔离级别是 serizlizable 时,就会将数据库访问性质为单线程,其原理就是采用了锁机制。当 A 的共享锁释放时,数据库才会为表记录再分配排它锁。如果 B 也是进行查询操作,那么此时就能成功查询,因为共享锁和共享锁不冲突。

 

         更新丢失问题:

         场景:老公去在 ATM 上取钱,老婆在柜台存钱,假设这个账户中有 1000 元。老公首先执行查询操作,查询到账户余额为 1000 此时程序将 1000 拿到内存中,老公取了 200 元,程序就执行了更新操作将账户余额改为 800,但是当老公的程序没有 commit 的时候,老婆查询账户,此时账户余额还是 1000 元,老婆存入 200 元,程序执行了更新操作将账户余额改为 1200,然后老公将更新语句提交,接着老婆也将更新语句提交。最后导致的结果就是该账户的余额为 1200,这就是更新丢失的问题。引发更新丢失的根源就是查询上,因为双方都是根据从数据库查询到的数据再对数据库中的数据进行更新的。解决更新丢失有三个方案:(1) 将事务隔离级别设置为最高,采用死锁策略。(2) 采用悲观锁,悲观锁不是数据库中真正的锁,是人们看待事务的态度。(3) 采用乐观锁,乐观锁也不是数据库中真正的锁。

          如果我们采用的是第一个方案时,老公进行查询操作,数据库为表增加了共享锁,老婆进行查询操作时数据库也增加了一个共享锁。但是当老公进行更新数据库操作时,由于老婆拿着共享锁,导致老公不能增加排它锁,老婆进行更新操作时,因为老公拿着共享锁,导致老婆也拿不到排它锁,这就发生了死锁现象,你等我,我等你。在 mysql 中,处理死锁的方案是释放掉一方的锁。这样就保证了一方更新成功,但是这种性能极低,因为数据库频繁在解决死锁问题。

         3、悲观锁(更新多,查询少时用)

              如果我们采用的是第二个方案时,即采用悲观锁。就是我们在操作数据库时采用悲观的态度,认为别人会在此时并发访问数据库。我们在查询语句中 select * from account where name='aaa' for update; 等于加了排它锁。当老公查询余额的时候,select money from account where name='aaa' for update; 增加了排它锁,老婆查询账户余额的时候, select money from account where name='aaa' for update;也要求对数据库加排它锁,因为老公已经拿到了排它锁,导致老婆不能加锁,所以老婆只有等待老公执行完毕,释放掉锁以后才能继续操作。

         4、乐观锁(更新少,查询多时用)

              如果我们采用的是第三个方案时,即采用乐观锁,就是我们在操作数据库的时候会认为没有其它用户并发访问,但是乐观锁也不是完全乐观的,乐观锁是采用版本号的方式进行控制的。在数据库表中有一列版本号。从数据库中查询的时候,将版本号也查询过来,在进行更新操作的时候,将版本号加1,查询条件的版本号还是查询过来的版本号。比如,老公执行查询操作的时候,select money,version from account where name='aaa'; 假设此时查询到的版本号为 0,老公在进行更新操作的时候 update account set money=money+100,version=version+1 where name='aaa' and version=0; 未提交时老婆来查询,查询到的版本号依然是 0,老婆也执行更新操作 update account set money=money+100,version=version+1 where name='aaa' and version=0; 现在老公提交了事务,老婆再提交事务的时候发现版本号为 0 的记录没有了,所以就避免了数据丢失的问题。不过这种情况也导致了多个用户更新操作时,只有一个用户的更新被执行。

         5、行级锁(为某一条记录加锁)

              如果想对数据库中的某条记录加行级锁,那么 where 条件后面必须为索引列。否则 for update 加的都是表级锁。行级锁就是只对要访问的当前行加锁,其他用户访问其它行记录的时候可以访问。 select * from account where id=1 for update;

         6、表级锁(为一张表加锁)

              在查询语句后增加 for update 时,where 条件后不是索引列,那么此时都是表级锁。select * from account where name='aaa' for update;



同类文章