zoukankan      html  css  js  c++  java
  • 事务的隔离级别,传播行为,锁机制

    一,事务的特性(ACID):

              原子性(Atomicity):

                       事务是一个完整的操作,事务的各步操作都是不可再分的,要么都执行, 要么都不执行。

             一致性(Consistency):

                       当事务完成时,数据必须处于一致的状态。

             隔离性(Isolation):

                       并发事务之间相互独立、隔离,它不应以任何方式依赖于或影响其他事务。

             持久性(Durability):

                       事务完成后,它对数据库的修改被永久保持。

       在Spring中,所有操作事务的类都继承自 PlatformTransactionManager

    二,事务的隔离级别

          ISOLATION_READ_UNCOMMITTED:读未提交       

          ISOLATION_READ_COMMITTED:读已提交

          ISOLATION_REPEATABLE_READ:可重复读 

          ISOLATION_SERIALIZABLE:串行化

    三,脏读,不可重复读,幻读(虚读)

      脏读:A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作,这时候如果事务B回滚,那么A事务读到的数据是不被承认的。

      不可重复读:不可重复读是指A事务读取了B事务已经提交的更改数据。假如A在取款事务的过程中,B往该账户转账100,A两次读取的余额发生不一致。

      幻读:A事务读取B事务提交的新增数据,会引发幻读问题。幻读一般发生在计算统计数据的事务中

    四,事务的传播行为

     五,锁机制

    悲观锁:

    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

    乐观锁:(不能解决脏读的问题)

    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制或CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

       (1)使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。或者在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

      (2)CAS算法即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

    需要读写的内存值 V

    进行比较的值 A

    拟写入的新值 B

    当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

    注:

    乐观锁适用于写比较少的情况下(并发量大/多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

    Mysql InnoDB引擎的锁机制(属于悲观锁)

    (之所以以InnoDB为主介绍锁,是因为InnoDB支持事务,支持行锁和表锁用的比较多,Myisam不支持事务,只支持表锁)

    (1)按照锁的使用方式可分为共享锁、排它锁意向共享锁意向排他锁

    共享锁/读锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。(其他事务可以读但不能写该数据集) 

    排他锁/写锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。 (其他事务不能读和写该数据集)

    意向共享锁(IS):通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加共享锁,那么此时innodb会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁。

    意向排他锁(IX):通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加排他锁,那么此时innodb会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。

    注:

    A、意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理;

    B、对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁,事务可以通过以下语句显示给记录集加共享锁或排他锁。

    共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。

    排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE。

    (2)按照锁的粒度分为行锁、页锁(间隙锁)、表锁

    行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件来检索数据才会用到行锁,否则InnoDB将会使用表锁。

    表锁:select * from table_nane where name = ‘小巷’  for update 。name字段不是唯一索引字段,所以是表锁。(表排他锁)

    行锁:select * from table_name where  id = 1 for update 。id 字段为唯一索引字段,所以使用的就是行锁,且是排它锁。

    页锁(又叫Gap锁/间隙锁:所谓表锁锁表,行锁锁行,那么页锁折中,锁相邻的一组数据。

    通过加锁控制,可以保证数据的一致性,但是同样一条数据,不论用什么样的锁,只可以并发读,并不可以读写并发(因为写的时候加的是排他锁所以不可以读),这时就要引入数据多版本控制来实现读写并发。

    MVCC数据多版本并发控制,属于乐观锁)

    这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证,换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性的强大的技术,因为这样的一来的话查询就不用等待另一个事务释放锁。

    数据多版本实现的原理是:

    1,写任务发生时,首先复制一份旧数据,以版本号区分

    2,写任务操作新克隆的数据,直至提交

    3,并发读的任务可以继续从旧数据(快照)读取数据,不至于堵塞

    注:

    快照读和当前读

    快照读:读取的是快照版本,也就是历史版本

    当前读:读取的是最新版本

    普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。

    具体实现:

    在InnoDB中,给每行增加两个隐藏字段来实现MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。在实际操作中,存储的并不是时间,而是事务的版本号(即创建版本号和删除版本号),每开启一个新事务,事务的版本号就会递增。(严格的来讲,InnoDB会给数据库中的每一行增加三个字段,它们分别是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID)

    于是乎,默认的隔离级别(REPEATABLE READ)下,增删查改变成了这样:

    SELECT

    读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。

    INSERT

    将当前事务的版本号保存至行的创建版本号

    UPDATE

    新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号

    DELETE

    将当前事务的版本号保存至行的删除版本号

    例如:

    此时books表中有5条数据,版本号为1

    事务A,系统版本号2:select * from books;因为1<=2所以此时会读取5条数据。

    事务B,系统版本号3:insert into books ...,插入一条数据,新插入的数据版本号为3,而其他的数据的版本号仍然是2,插入完成之后commit,事务结束。

    事务A,系统版本号2:再次select * from books;只能读取<=2的数据,事务B新插入的那条数据版本号为3,因此读不出来,解决了幻读的问题。

    注:

    排它锁 是 串行执行

    共享锁 是 读读并发

    数据多版本 是 读写并发

    乐观锁利用MVCC实现一致性非锁定读,这就有保证在同一个事务中多次读取相同的数据返回的结果是一样的,解决了不可重复读的问题,也可以解决幻读问题;悲观锁,serializable隔离级别,利用Gap Locks(页锁)、表锁可以阻止其它事务在锁定区间内插入数据,因此解决了幻读问题。

    总结:

    数据库并发问题,主要通过设置事务隔离级别来解决,而事务隔离级别一般则通过锁机制的实现;

    MySQL默认隔离级别(RR)使用MVCC+锁混合的模式来解决脏读、不可重读、幻读等问题。

    MySQL(Innodb引擎)下

    默认的事务级别为:可重复读级别(RR);(可通过设置进行更改)

    默认锁级别为:行锁;(可通过设置进行更改)

    Where筛选条件中使用索引字段的,加的是行锁;不是使用索引字段筛选的,加的是表锁。

    意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理;

    对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;

    对于普通SELECT语句,InnoDB不会加任何锁;(可以自己手动上锁)

    注:

    READ UNCOMMITTED 读未提交 read-uncommitted

    READ COMMITTED 读已提交 read-committed

    REPEATABLE READ 可重复读 repeatable-read

    SERIALIZABLE 串行化 serializable

    查看数据库全局事务隔离级别:

    SELECT @@GLOBAL.tx_isolation;

    查看数据库当前会话事务隔离级别:

    SELECT @@SESSION.tx_isolation;

    SELECT @@tx_isolation;

    设置全局事务隔离级别:

    SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

    设置当前会话事务隔离级别:

    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

    SET tx_isolation = 'read-committed';

  • 相关阅读:
    Servlet项目 创建方法
    1. 连接数据库
    Jsp application对象(全局变量)
    Jsp session属性、方法
    让python 3支持mysqldb的解决方法
    Python读写文件(进阶)
    python操作MongoDB
    使用Python Pandas处理亿级数据
    SurfingTheInternet
    matplotlib中日期显示(不显示为科学计数法)
  • 原文地址:https://www.cnblogs.com/liuying23/p/11792616.html
Copyright © 2011-2022 走看看