mysql数据库隔离级别及其原理、Spring的7种事物传播行为

1、事务的基本要素(ACID)html

  一、原子性(Atomicity):事务开始后全部操做,要么所有作完,要么所有不作,不可能停滞在中间环节。事务执行过程当中出错,会回滚到事务开始前的状态,全部的操做就像没有发生同样。也就是说事务是一个不可分割的总体,就像化学中学过的原子,是物质构成的基本单位。mysql

   二、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。好比A向B转帐,不可能A扣了钱,B却没收到。
算法

 

   三、隔离性(Isolation):同一时间,只容许一个事务请求同一数据,不一样的事务之间彼此没有任何干扰。好比A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转帐。sql

 

   四、持久性(Durability):事务完成后,事务对数据库的全部更新将被保存到数据库,不能回滚。数据库

 

2、事务的并发问题编程

  一、脏读:一个事务读取另一个事务尚未提交的数据叫脏读【针对未提交的数据segmentfault

  二、不可重复读:即在同一个事务内,两个相同的查询返回了不一样的结果【读取数据自己的对比                并发

    表tms_message_info数据:
ide

      ID    file_name性能

                1     1111.SND

                 2    2222.SND

 

 

事务A READ-COMMITTED 事务B READ-COMMITTED

start transaction;

select * from tms_message_info where id='1'

start transaction;-- 开启事务

update tms_message_info set FILE_NAME='666.SND' where ID='1'; 未提交

B事务未提交前,A事务执行上述查询,获得的文件名结果为1111.SND commit;  B 事务提交
B事务提交后,A事务在执行查询,结果发生变化,文件名变为666.SND,即同一事务查询结果不一样,即不可重复读  

 

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

  【同一事务A屡次查询,若另外一事务B只是update,则A事务屡次查询结果相同;若B事务insert/delete数据,则A事务屡次查询就会发现新增或缺乏数据,出现幻读,即幻读关注读取结果集条数变化

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

 

3、MySQL事务隔离级别

 

一、读不提交(Read Uncommited,RU)

 

  这种隔离级别下,事务间彻底不隔离,会产生脏读,能够读取未提交的记录,实际状况下不会使用。

 

二、读提交(Read commited,RC)

 

  本事务读取到的是最新的数据(其余事务提交后的)。问题是,在同一个事务里,先后两次相同的SELECT会读到不一样的结果(不重复读)

 

三、可重复读(Repeatable Read,RR)【MySQL 默认的级别】

 

  在同一个事务里,SELECT的结果是事务开始时时间点的状态,所以,同一个事务一样的SELECT操做读到的结果会是一致的。可是,会有幻读现象

 

四、 串行化(SERIALIZABLE)。读操做会隐式获取共享锁,能够保证不一样事务间的互斥

 

 

 

 

4、4种隔离级别的相应原理总结以下:

READ_UNCOMMITED 的原理:

  • 事务对当前被读取的数据不加锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级共享锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,能读到事务2对该记录的修改版本,即便该修改还没有被提交。
  • 事务1更新某行记录时,事务2不能对这行记录作更新,直到事务1结束。

READ_COMMITED 的原理:

  • 事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,当即释放该行级共享锁;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,读到的只能是事务2对其更新前的版本,要不就是事务2提交后的版本。
  • 事务1更新某行记录时,事务2不能对这行记录作更新,直到事务1结束。

REPEATABLE READ 的原理:

  • 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加 行级共享锁,直到事务结束才释放;
  • 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。

表现:

  • 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,读到的仍然是第一次读取的那个版本。
  • 事务1更新某行记录时,事务2不能对这行记录作更新,直到事务1结束。

SERIALIZABLE 的原理:

  • 事务在读取数据时,必须先对其加 表级共享锁 ,直到事务结束才释放;
  • 事务在更新数据时,必须先对其加 表级排他锁 ,直到事务结束才释放。

表现:

  • 事务1正在读取A表中的记录时,则事务2也能读取A表,但不能对A表作更新、新增、删除,直到事务1结束。
  • 事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,更不可能对A表作更新、新增、删除,直到事务1结束。

 

注释:

一、共享锁:称为读锁,简称S锁,顾名思义,共享锁就是若是事务T对数据A加上共享锁后,则其余事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。共享锁也属于悲观锁的一种

 

ps:mysql中 select * from xx where id =1 lock in share mode便可给该数据加共享锁,在该事务提交以前,不容许其余事务进行修改。
 

 

二、排他锁:又称为写锁,简称X锁,顾名思义,排他锁就是不能与其余所并存,如一个事务获取了一个数据行的排他锁,其余事务就不能给该数据加其余锁,包括共享锁和排他锁,可是获取排他锁的事务是能够对数据就行读取和修改。

ps:对于update,insert,delete语句会自动加排它锁)。添加排它锁与添加共享锁相似,执行语句添加 “ for update” 便可。

 

对于共享锁你们可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁你们的理解可能就有些差异,我当初就犯了一个错误,觉得排他锁锁住一行数据后,其余事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其余事务不能再在其上加其余的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,若是加排他锁可使用select ...for update语句,加共享锁可使用select ... lock in share mode语句。因此加过排他锁的数据行在其余事务种是不能修改数据的,也不能经过for update和lock in share mode锁的方式查询数据,但能够直接经过select ...from...查询数据,由于普通查询没有任何锁机制。

 

 

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

(另外:同步锁synchronized和重入锁ReentrantLock的相同点与不一样点:

    相同点:1)ReentrantLock和synchronized都是独占锁,只容许线程互斥的访问临界区

        2)ReentrantLock和synchronized都是可重入的

    不一样点:1)ReentrantLock能够实现公平锁。公平锁是指当锁可用时,在锁上等待时间最长的线程将得到锁的使用权。而非公平锁则随机分配这种使用权。默认状况下两者都是非公平锁,可是ReentrantLock能够设置为公平锁

        2)ReentrantLock可响应中断。当使用synchronized实现锁时,阻塞在锁上的线程除非得到锁不然将一直等待下去,也就是说这种无限等待获取锁的行为没法被中断。而ReentrantLock给咱们提供了一个能够响应中断的获取锁的方法 

          lockInterruptibly()。该方法能够用来解决死锁问题。

        3)获取锁时限时等待。ReentrantLock还给咱们提供了获取锁限时等待的方法tryLock(),能够选择传入时间参数,表示等待指定的时间,无参则表示当即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。咱们可使用该方法配合失败重试机制         来更好的解决死锁问题。

        4)结合Condition实现等待通知机制。    

        

Condition接口在使用前必须先调用ReentrantLock的lock()方法得到锁。以后调用Condition接口的await()将释放锁,而且在该Condition上等待,直到有其余线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify相似

public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("主线程等待通知");
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println("主线程恢复运行");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("子线程通知");
            } finally {
                lock.unlock();
            }
        }
    }
}

//执行结果
主线程等待通知
子线程通知
主线程恢复运行
View Code

 

乐观锁:所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。

方法1:加版本号

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};

 方法2:cas算法(即比较与交换):一种无锁算法

CAS算法涉及到三个操做数

    须要读写的内存值 V
    进行比较的值 A
    拟写入的新值 B

当且仅当 V 的值等于 A时,CAS经过原子方式用新值B来更新V的值,不然不会执行任何操做(比较和替换是一个原子操做)。通常状况下是一个自旋操做,即不断的重试。
public final int getAndIncrement() {
   for (;;) {
    int current = get();
    int next = current + 1;
    if (compareAndSet(current, next))
    return current;
  }
}

乐观锁缺点:1)ABA问题:若是一个变量V初次读取的时候是A值,而且在准备赋值的时候检查到它仍然是A值,那咱们就能说明它的值没有被其余线程修改过了吗?很明显是不能的,由于在这段时间它的值可能被改成其余值,而后又改回A,那CAS操做就会误认为它历来没有被修改过。这个问题被称为CAS操做的 "ABA"问题。

      2)自旋CAS(也就是不成功就一直循环执行直到成功)若是长时间不成功,会给CPU带来很是大的执行开销

       3)只能保证一个共享变量的原子操做

另外:CAS(乐观锁)与synchronized(悲观锁)的使用情景

简单的来讲CAS适用于写比较少的状况下(多读场景,冲突通常较少),synchronized适用于写比较多的状况下(多写场景,冲突通常较多)

1)对于资源竞争较少(线程冲突较轻)的状况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操做额外浪费消耗cpu资源;而CAS基于硬件实现,不须要进入内核,不须要切换线程,操做自旋概率较少,所以能够得到更高的性能。
2)对于资源竞争严重(线程冲突严重)的状况,CAS自旋的几率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

 

 

 

注意:mysql的update、delete、insert是默认加了排他锁的,而普通的selete是什么锁都没加,因此普通selete既能查共享锁的行也能查排他锁的行

参考:https://www.cnblogs.com/lyftest/p/7687747.html,https://www.cnblogs.com/lyftest/p/7687747.html

https://blog.csdn.net/weixin_39666581/article/details/87008846

https://blog.csdn.net/u013030086/article/details/86492335

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Spring的7种事物传播行为:

参考:https://segmentfault.com/a/1190000013341344

    Spring有7中事物传播行为:由@Transaction(Propagation=XXX)设置决定:
    1)PROPAGATION_REQUIRED:若是当前没有事务,就新建一个事务,若是已经存在一个事务中,加入到这个事务中。这是最多见的选择。即:无论内部事物仍是外部事物,只要发生异常,所有回滚
    2)PROPAGATION_REQUIRES_NEW:新建事务,若是当前存在事务,把当前事务挂起。
    3)PROPAGATION_SUPPORTS:支持当前事物,若是没有事物,就以非事物方式执行。
    4)PROPAGATION_MANDATORY:使用当前事物,没有就抛出异常
    5)PROPAGATION_NOT_SUPPORTED:使用非事物的方式执行,若是当前存在事物,就把当前事物挂起
    6)PROPAGATION_NEVER:以非事物方式执行,若是当前事物存在,则抛出异常
    7)PROPAGATION_NESTED:若是当前存在事物,则嵌套事物内部执行;若是当前没有事物,则执行与PROPAGATION_REQUIRED相似操做
    https://segmentfault.com/a/1190000013341344

    REQUIRED,REQUIRES_NEW,NESTED异同
  NESTEDREQUIRED修饰的内部方法都属于外围方法事务,若是外围方法抛出异常,这两种方法的事务都会被回滚。可是REQUIRED是加入外围方法事务,因此和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,因此NESTED方法抛出异常被回滚,不会影响到外围方法的事务。

  NESTEDREQUIRES_NEW均可以作到内部方法事务回滚而不影响外围方法事务。可是由于NESTED是嵌套事务,因此外围方法回滚以后,做为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是经过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。

 

事务能够分为编程式事务和声明式事务。

关于声明式事物@Transactional两个注意事项

一、在Service层抛出Exception,在Controller层捕获,那若是在Service中有异常,那会事务回滚吗?

结论:若是是编译时异常不会自动回滚,若是是运行时异常,那会自动回滚

二、若是我在当前类下使用一个没有事务的方法去调用一个有事务的方法,那咱们此次调用会怎么样?是否会有事务呢?

结论:若是是在同一个service中(即在同一个@Service下),一个没有事物的方法调用一个有事物的方法,那么事物不会生效;若是再也不同一个service中(即在不一样的@Service),则会生效。

缘由:

咱们都知道,带有@Transactional注解所包围的方法就能被Spring事务管理起来,那若是我在当前类下使用一个没有事务的方法去调用一个有事务的方法,那咱们此次调用会怎么样?是否会有事务呢?

用代码来描述一下:

// 没有事务的方法去调用有事务的方法
public Employee addEmployee2Controller() throws Exception {

    return this.addEmployee();
}

@Transactional
public Employee addEmployee() throws Exception {

    employeeRepository.deleteAll();
    Employee employee = new Employee("3y", 23);

    // 模拟异常
    int i = 1 / 0;

    return employee;
}

 

我第一直觉是:这跟Spring事务的传播机制有关吧。

其实这跟Spring事务的传播机制没有关系,下面我讲述一下:

  • Spring事务管理用的是AOP,AOP底层用的是动态代理。因此若是咱们在类或者方法上标注注解@Transactional,那么会生成一个代理对象

接下来我用图来讲明一下:

 

 显然地,咱们拿到的是代理(Proxy)对象,调用addEmployee2Controller()方法,而addEmployee2Controller()方法的逻辑是target.addEmployee(),调用回原始对象(target)的addEmployee()。因此此次的调用压根就没有事务存在,更谈不上说Spring事务传播机制了。

 从上面的测试咱们能够发现:若是是在本类中没有事务的方法来调用标注注解@Transactional方法,最后的结论是没有事务的。那若是我将这个标注注解的方法移到别的Service对象上,有没有事务?

@Service
public class TestService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Transactional
    public Employee addEmployee() throws Exception {

        employeeRepository.deleteAll();

        Employee employee = new Employee("3y", 23);

        // 模拟异常
        int i = 1 / 0;

        return employee;
    }

}


@Service
public class EmployeeService {

    @Autowired
    private TestService testService;
    // 没有事务的方法去调用别的类有事务的方法
    public Employee addEmployee2Controller() throws Exception {
        return testService.addEmployee();
    }
}

 

这时就会有事务了。

由于咱们用的是代理对象(Proxy)去调用addEmployee()方法,那就固然有事务了。

相关文章
相关标签/搜索