数据库——事务和锁

目录

事务(Transaction)

乐观锁(Optimistic Lock)

悲观锁(Pessimistic Lock)

封锁(数据库机制锁)

封锁协议

死锁(Deadlock)

MySQL


事务(Transaction)

概念定义:事务是恢复和并发控制的基本单位

基本特性:ACID(原子性,一致性,隔离性,持久性)

  • 原子性和一致性差不多,意思是要么全部成功,要么就全部失败

  • 一致性是说,从一个一致性状态到另一个一致性状态,即状态的一致性

  • 隔离性是说一个事务执行的过程中不能被另一个事务干扰,我理解为概率事件中的独立性

  • 持久性也就是事务一旦提交,他对数据库中数据的改变就应该是永久的

事务并发:导致数据不一致性

  • 丢失更新:当两个或者多个事务同时对某一数据进行更新的时候,事务B的更新可能覆盖掉事务A的更新;
  • 脏读:破坏了事务的隔离性,事务T1修改数据A后,释放写锁,事务T2读取数据A,此时事务T1回滚,事务2就读到了脏数据;
  • 不可重复读:事务T1读取数据A后,事务T2修改了A,T1再去读A时,得到与前一次不同的值;
  • 幻读:当某个事务在读取某个范围内的记录的时候,另外一个事务在这个范围内增加了一行或删除了一行,当前一个事务再次读取该范围的数据的时候就会发生幻读。

乐观锁(Optimistic Lock)

概念定义:每次获取数据的时候都不会进行加锁,但是在更新数据前需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

应用场景:比较适合读取操作比较频繁且并发量较大的场景,可以提高系统的吞吐量;反之如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,反而降低了系统的吞吐量。

实现方式:

  • 版本号控制:在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会+1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

问题思考它如何保证检查版本,提交和修改版本是一个原子操作? 也就是如何保证在检查版本的期间,没有其他事务对其进行操作?

解决方案: 将比较版本号和更新操作写入到同一条SQL语句中可以解决该问题,

update table1 set a=1, b=2, version = version +1 where version = 1; 

mysql 自己能够保障单条SQL语句的原子操作性

如果是多条SQL语句,就需要mySQL的事务通过锁机制来保障了

  • CAS(compare and swap):也叫非阻塞同步(Non-blocking Synchronization)或者无锁算法,在JUC.atomic包下面的原子变量类就是采用这种方式实现的。CAS算法涉及到三个操作数:需要读写的数V(第一次取出)、进行比较的数A(提交前进行比较,再次取出的数)、修改提交的新值B,当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

CAS带来的问题又有哪些?

  • ABA 问题(理解ABA)

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

解决:1、JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。2、互斥同步锁synchronized。3、如果业务逻辑不是过于复杂即只在乎数值的正确性的话,可能不会影响并发正确性。(参考:ABA解决)

  • 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 

  • 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了Atomic Reference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

CAS与Synchronized的使用场景?

  • CAS适用于写比较少的情况下(多读场景,冲突一般较少)
  • Synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

解释:对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。参考(乐观悲观比较)
 

悲观锁(Pessimistic Lock)

概念定义:每次获取数据的时候会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

应用场景:比较适合写入操作比较频繁或者并发量较小的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

封锁(数据库机制锁)

共享锁(share lock        读锁):如果事务T对数据对象A加上读锁,T可以读A但不能写A,其他事务可以加读锁,但是不能加写锁,直到释放该数据上所有的读锁。通俗来说就是多个用户可以同时读,但是不能有人修改。

排它锁(exclusive lock  写锁):如果事务T对数据对象A加上写锁,那么只允许事务T读取和修改A,其他任何事务不能再对A加任何类型的锁,直到T释放A上的锁为止。通俗来说就是只有一个用户来写,其他用户不能读也不能写。

封锁协议

一级封锁协议:修改数据之前加写锁,事务结束释放(包括正常结束commit和回滚rollback)。可能会读脏数据,不支持重复读

二级封锁协议:在一的基础上,读数据之前加读锁,读完后释放。不保证可重复度

三级封锁协议:在一的基础上,读数据之前加读锁,直到事务结束才释放(比如事务中有验算过程,需要重复读一个数据)

死锁(Deadlock)

概念定义:死锁是指一个进程(或者线程)无限期的等待另一个进程所占有的永远不会释放的资源;

产生原因一种是竞争资源,共享的资源不足引起竞争而产生死锁,另一种是进程推进顺序不当,请求和释放的顺序不当引起的死锁;

必要条件互斥、请求和保持、不剥夺、环路等待;

解决办法预防、避免、检测和解除四个方面,预防就是破坏死锁产生的四个必要条件,但会降低系统的资源利用率,避免的话就是不破坏条件,而是在资源动态分配的过程中防止系统进入不安全的状态,比如经典的银行家算法,检测和解除是配套的一种设施,就是通过检测机构确定死锁的进程或者资源然后再予以解除,常用的就是挂起或者撤销一些进程,从而释放一些资源。

MySQL

数据结构:B+树

  • 聚簇索引:
  • 非聚簇索引:

数据库引擎:

  • innodb :支持事务,支持到行锁,支持binlog日志;
  • myisam:不支持事务,支持到表锁,查询select count(*)速度最快;

加锁粒度:(experience)

  • 表锁:开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高;
  • 页锁:是MySQL中比较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁
  • 行锁:  开销较大,能具体的锁定到表中的某一行数据,但是能更好的支持并发处理, 会发生死锁

隔离级别:MySQL默认可重复读

  • 未提交读(Read Uncommitted) 事务之间的数据是相互可见的;
  • 提交读(Read Commit)大多数数据库的默认隔离级别, 保证了不可能脏读,但是不能保证可重复读, 在这个级别里,数据的加锁实现是读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
  • 可重复读(Repeatable Read) 解决了不可重复读的问题,保证了在同一个事务之中,多次读取相同的记录的值的结果是一致的。 但是无法解决幻读。这个阶段的事务隔离性,在mysql中是通过基于乐观锁原理的多版本控制实现的。
  • 可串行化读(Serializable) 最高的隔离级别,解决了幻读 ,它会在读取的每一行数据上都进行加锁, 有可能导致超时和锁争用的问题。它的加锁实现是读取的时候加共享锁,修改删除更新的时候加排他锁,读写互斥,但是并发能力差。

 

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

加锁协议:

  • 一次封锁协议:为了预防死锁,使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式虽然可以有效的避免死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据,势必会扩大封锁的范围,降低了系统的并发度。
  • 两段锁协议:实现事务并发调度的可串行性,从而保证调度正确性(数据恢复和备份的时候用到它),可能发生死锁。事务分两个阶段对数据项进行加锁和解锁

          1. 加锁阶段:对任何数据进行读写操作之前,首先要申请并获得对该数据的封锁;

          2. 解锁阶段:释放了一个封锁以后,事务进入解锁阶段,不再申请和获得任何其他锁。

       例如:事务提交时(commit) 和事务回滚时(rollback)会自动的同时释放该事务所加的insert、update、delete对应的锁。
 

你可能感兴趣的:(吾日三省吾身)