并发控制: 事务和锁的存在都是为了更好的解决并发访问造成的数据不一致性的的问题
乐观锁和悲观锁都是为了解决并发控制问题, 乐观锁可以认为是一种在最后提交的时候检测冲突的手段,而悲观锁则是一种避免冲突的手段。
乐观锁: 是应用系统层面和数据的业务逻辑层次上的(实际上并没有加锁,只不过大家一直这样叫而已),利用程序处理并发, 它假定当某一个用户去读取某一个数据的时候,其他的用户不会来访问修改这个数据,但是在最后进行事务的提交的时候会进行版本的检查,以判断在该用户的操作过程中,没有其他用户修改了这个数据。开销比较小
乐观锁的实现大部分都是基于版本控制实现的,
乐观锁的优势和劣势
优势:如果数据库记录始终处于悲观锁加锁状态,可以想见,如果面对几百上千个并发,那么要不断的加锁减锁,而且用户等待的时间会非常的长, 乐观锁机制避免了长事务中的数据库加锁解锁开销,大大提升了大并发量下的系统整体性能表现 所以如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以建议就要选择乐观锁定的方法, 而如果并发量不大,完全可以使用悲观锁定的方法。乐观锁也适合于读比较多的场景。
劣势: 但是乐观锁也存在着问题,只能在提交数据时才发现业务事务将要失败,如果系统的冲突非常的多,而且一旦冲突就要因为重新计算提交而造成较大的代价的话,乐观锁也会带来很大的问题,在某些情况下,发现失败太迟的代价会非常的大。而且乐观锁也无法解决脏读的问题
悲观锁: 完全依赖于数据库锁的机制实现的,在数据库中可以使用Repeatable Read的隔离级别(可重复读)来实现悲观锁,它完全满足悲观锁的要求(加锁)。
它认为当某一用户读取某一数据的时候,其他用户也会对该数据进行访问,所以在读取的时候就对数据进行加锁, 在该用户读取数据的期间,其他任何用户都不能来修改该数据,但是其他用户是可以读取该数据的, 只有当自己读取完毕才释放锁。
悲观锁的优势和劣势
劣势:开销较大,而且加锁时间较长,对于并发的访问性支持不好。
优势 : 能避免冲突的发生
乐观锁和悲观所各有优缺点,在乐观锁和悲观锁之间进行选择的标准是:发生冲突的频率与严重性。
如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突太多或者冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略,它能避免冲突的发生。 如果要求能够支持高并发,那么乐观锁 。
其实使用乐观锁 高并发==高冲突, 看看你怎么衡量了
共享锁和排它锁是具体的锁,是数据库机制上的锁。
共享锁(读锁) 在同一个时间段内,多个用户可以读取同一个资源,读取的过程中数据不会发生任何变化。读锁之间是相互不阻塞的, 多个用户可以同时读,但是不能允许有人修改, 任何事务都不允许获得数据上的排它锁,直到数据上释放掉所有的共享锁
排它锁(写锁) 在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他的读锁或者写锁操作,只能由这一个用户来写,其他用户既不能读也不能写。
加锁会有粒度问题,从粒度上从大到小可以划分为
表锁 开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高。
页锁:是MySQL中比较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。
行锁 开销较大,能具体的锁定到表中的某一行数据,但是能更好的支持并发处理, 会发生死锁
事物: 用于保证数据库的一致性
所谓数据一致性,就是当多个用户试图同时访问一个数据库,它们的事务同时使用相同的数据时,可能会发生以下四种情况:丢失更新、脏读、不可重复读 和 幻读
所谓数据完整性, 数据库中的数据是从外界输入的,而数据的输入由于种种原因,会发生输入无效或错误信息。保证输入的数据符合规定,
数据完整性分为四类:实体完整性(Entity Integrity)、域完整性(Domain Integrity)、参照完整性(Referential Integrity)、用户定义的完整性(User-definedIntegrity)。
数据库采用多种方法来保证数据完整性,包括外键、约束、规则和触发器。
事务的ACID特性
原子性Automicity,一个事务内的所有操作,要么全做,要么全不做
一致性Consistency,数据库从一个一致性状态转到另一个一致性状态
独立性(隔离性)isolation, 一个事务在执行期间,对于其他事务来说是不可见的
(Durability): 事务一旦成功提交,则就会永久性的对数据库进行了修改
隔离级别: mySQL默认的隔离级别是可重复读
在SQL 中定义了四种隔离级别;
READ UNCOMMITED(未提交度) 事务之间的数据是相互可见的
READ COMMITED(提交读) 大多数数据库的默认隔离级别, 保证了不可能脏读,但是不能保证可重复读, 在这个级别里,数据的加锁实现是读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
REPEATABLE READ (可重复读) 解决了不可重复读的问题,保证了在同一个事务之中,多次读取相同的记录的值的结果是一致的。 但是无法解决幻读。这个阶段的事务隔离性,在mysql中是通过基于乐观锁原理的多版本控制实现的。
SERIALIZABLE (可串行化读) 最高的隔离级别,解决了幻读 ,它会在读取的每一行数据上都进行加锁, 有可能导致超时和锁争用的问题。
它的加锁实现是读取的时候加共享锁,修改删除更新的时候加排他锁,读写互斥,但是并发能力差。
丢失更新: 当两个或者多个事务同时对某一数据进行更新的时候,事务B的更新可能覆盖掉事务A的更新,导致更新丢失
解决方案:
悲观锁的方式: 加锁,建议最后一步更新数据的时候加上排它锁,不要在一开始就加锁
执行到了最后一步更新,首先做一下加锁的查询确认数据有没有没改变,如果没有被改变,则进行数据的更新,否则失败。 一定要是做加锁的查询确认,因为如果你不加锁的话,有可能你在做确认的时候数据又发生了改变。
乐观锁的方式:使用版本控制实现
脏读: 事务可以读取未提交的数据,比如:
事务A对某一个数据data=1000 进行了修改: data = 2000, 但是还没有提交;
事务B读取data 得到了结果data = 2000,
由于某种原因事务A撤销了刚才的操作,数据data = 1000 然后提交
这时事务B读取到的2000就是脏数据。正确的数据应该还是 1000
解决方法 : 把数据库的事务隔离级别调整到READ_COMMITTED , 但是存在事务A与B都读取了data,A还未完成事务,B此时修改了数据data,并提交, A又读取了data,发现data不一致了,出现了不可重复读。
不可重复读 : 在同一个事务之中,多次读取相同的记录的值的结果是不一样的,针对的是数据的修改和删除。
事务A 读取data = 1000, 事务还未完成;
事务B 修改了data = 2000, 修改完毕事务提交;
事务A 再次读取data, 发现data = 2000 了,与之前的读取不一致的
解决办法; 把数据库的事务隔离级别调整到 REPEATABLE READ , 读取时候不允许其他事务修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题
幻读: 当某个事务在读取某个范围内的记录的时候,另外一个事务在这个范围内增加了一行,当前一个事务再次读取该范围的数据的时候就会发生幻行,. 针对的是数据的插入insert
解决方案 : 采用的是范围锁 RangeS RangeS_S模式,锁定检索范围为只读 或者 把数据库的事务隔离级别调整到SERIALIZABLE_READ, MySQL中InnoDB 和 XtraDB 利用(多版本并发控制)解决了幻读问题,
加锁协议
一次封锁协议:因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。\
两段锁协议 将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
1. 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁(只有当前数据无共享锁,无排它锁之后才能获得),其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
2. 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
事务提交时(commit) 和事务回滚时(rollback)会自动的同时释放该事务所加的insert、update、delete对应的锁。
这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。
死锁 指两个事务或者多个事务在同一资源上相互占用,并请求对方所占用的资源,从而造成恶性循环的现象。
出现死锁的原因:
1. 系统资源不足
2. 进程运行推进的顺序不当
3. 资源分配不当
产生死锁的四个必要条件
1. 互斥条件: 一个资源只能被一个进程使用
2. 请求和保持条件:进行获得一定资源,又对其他资源发起了请求,但是其他资源被其他线程占用,请求阻塞,但是也不会释放自己占用的资源。
3. 不可剥夺条件: 指进程所获得的资源,不可能被其他进程剥夺,只能自己释放
4. 环路等待条件: 进程发生死锁,必然存在着进程-资源之间的环形链
处理死锁的方法: 预防,避免,检查,解除死锁