这篇文章详细介绍一下数据库事务与锁的相关知识。主要是一些概念性的东西看起来可能比较乏味,但作为一名合格的程序员来说,你应该掌握也必须掌握。这些理论知识好比是一个人的内功,我们平时敲代码是外功,只有内外兼修,相互促进,才能达到武林高手的境界。好了废话不多说,下面开始。
数据库事务
事务的边界
事务的开始边界(begin)
事务的结束边界(commit):提交事务,永久保存被事务更新后的数据库状态。
事务的异常结束边界(rollback):撤销事务,使数据库退回到执行事务前的初始状态。
每启动一个MySQL.exe程序,就会得到一个单独的数据库连接。每个数据库连接都有一个全局变量autocommit,表示当前的事务模式,它有两个值可选:
0:表示手工提交模式
1:表示自动提交模式,默认值
我们可以查看和修改这个值。
数据库事务的4个特性(ACID):
原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全部执行,要么全都不执行;
一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态;
隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行;
持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
事务隔离级别
数据库事务隔离级别,只是针对一个事务能不能读取其它事务的中间结果。
Read Uncommitted (读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读( Dirty Read )。
Read Committed (读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读( Nonrepeatable Read ),因为同一事务的其他实例在该实例处理其间可能会有新的 commit ,所以同一 select 可能返回不同结果。
Repeatable Read (可重读)
这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 ( Phantom Read )。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的 ” 幻影 ” 行。 InnoDB和 Falcon 存储引擎通过多版本并发控制( MVCC , Multiversion Concurrency Control )机制解决了该问题。
Serializable (可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
对于多数应用程序,可以有效考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、虚度和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁和乐观锁来控制。
事务的传播性
PROPAGATION_REQUIRED
加入当前正要执行的事务,如果当前事务不存在,那么就起一个新的事务。Spring 操作数据库默认的事务传播行为就是 propagation_required 。
PROPAGATION_SUPPORTS
如果当前在事务中,即以事务的形式运行,如果当前不再一个事务中,那么就以非事务的形式运行.
PROPAGATION_MANDATORY
必须在一个事务中运行。也就是说,他只能被一个父事务调用。否则,他就要抛出异常。
PROPAGATION_REQUIRES_NEW
挂起当前事务,另起一个新的事务。
PROPAGATION_NOT_SUPPORTED
当前不支持事务。如果在事务中,会挂起当前事务,自己以非事务的行为运行。
PROPAGATION_NEVER
不能在事务中运行,如果在事务中运行就会抛出异常。
PROPAGATION_NESTED
嵌套的事务依赖父事务,父事务提交,它跟着提交,父事务回滚,它跟着回滚。
行级锁
Mysql中三种类型的锁:
行级:引擎 INNODB , 单独的一行记录加锁
页级:引擎 BDB,一次锁定相邻的一组记录。
表级:引擎 MyISAM , 理解为锁住整个表,可以同时读,写不行。
三种锁的特性可大致归纳如下:
1) 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
2) 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
3) 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
我们这里主要谈论的是行级锁,一般的在秒杀系统中我们会对商品库存使用行级锁,因为秒杀的时候库存是一个很重要的数据,我们在创建数据库的表时可能会出现下面这样的设置:
ENGINE = InnoDB AUTO_INCREMENT=10 DEFAULT CHARACTER SET = utf8 comment='用户表
将引擎设置为InnoDB,InnnoDB与其他引擎的不同:一是支持事务(TRANCSACTION),二是采用了行级锁。
InnoDB中两种模式的行级锁:
1)共享锁:允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
( Select * from table_name where ……lock in share mode)
2)排他锁:允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和 排他写锁。(select * from table_name where…..for update)
为了允许行锁和表锁共存,实现多粒度锁机制;同时还有两种内部使用的意向锁(都是表锁),分别为意向共享锁和意向排他锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
注意:InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
行级锁的优缺点
行级锁定的优点:
当在许多线程中访问不同的行时只存在少量锁定冲突。
回滚时只有少量的更改。
可以长时间锁定单一的行。
行级锁定的缺点:
比页级或表级锁定占用更多的内存。
当在表的大部分数据上使用时,比页级或表级锁定速度慢,因为你必须获取更多的锁。如果你在大部分数据上经常进行GROUP BY操作或
者必须经常扫描整个表,比其它锁定明显慢很多。
hibernate中通过行级锁实现的悲观锁。
一些例子:
假设有个表单products ,里面有id跟name二个栏位,id是主键。
1: 明确指定主键,并且有此条记录,执行row lock。若查无此记录,无lock。
SELECT * FROM products WHERE id='3' FOR UPDATE;SELECT * FROM products WHERE id='3' and name="cat" FOR UPDATE;
2: 无主键,执行table lock。
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
3: 主键不明确,table lock。
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
注意: FOR UPDATE仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。此外,如果A与B都对表id进行查询但查询不到记录,则A与B在查询上不会进行row锁,但A与B都会获取排它锁,此时A再插入一条记录的话则会因为B已经有锁而处于等待中,此时B再插入一条同样的数据则会抛出Deadlock found when trying to get lock; try restarting transaction。然后释放锁,此时A就获得了锁而插入成功。
以上就是MySQL中的事务与锁的内容,更多相关内容请关注PHP中文网(www.php.cn)!