库存-Mysql中的事务、锁与存储引擎

3,806 阅读16分钟

设计一个库存系统。在库存系统中,最重要的就是要防止超卖。模拟的SQL语句如下:
首先查询是否有剩余量,正常的操作为:

select  *  from t_goods where id=1 and  rest>0

然后发现有剩余量,开始执行更新操作:

update t_goods   set rest=rest-1 where id=1;

假设有A,B,C 三个用户进来,那么同时执行select语句,假设剩余量只剩下1个,那么A,B,C同时操作会引起超卖。

那么可以在update的时候严格一下,也就是在update的时候再去判断一次库存是否大于0:

update t_goods   set rest=rest-1 where id=1 and  rest > 0;

那么,一个线程在update的时候,另外一个线程同时能访问这条数据吗?这看起来是一个简单的问题,然而要清楚明白的了解发生了什么,却并不容易。
这里就涉及到了数据库的三大板块:事务、,存储引擎

先来说说什么是事务,或者说把一个操作叫做事务,应该满足什么特征。

一、事务的4个基本特征

  当事务处理系统创建事务时,将确保事务具有某些特性。组件的开发者们假设事务的特性应该是一些不需要他们亲自管理的特性。这些特性称为ACID特性。 ACID就是:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily)。

1、原子性 (Atomicity )

   原子性属性用于标识事务是否完全地完成,一个事务的任何更新要在系统上完全完成,如果由于某种原因出错,事务不能完成它的全部任务,系统将返回到事务开始前的状态。让我们再看一下银行转帐的例子。如果在转帐的过程中出现错误,整个事务将会回滚。只有当事务中的所有部分都成功执行了,才将事务写入磁盘并使变化 永久化。为了提供回滚或者撤消未提交的变化的能力,许多数据源采用日志机制。例如,SQL Server使用一个预写事务日志,在将数据应用于(或提交到)实际数据页面前,先写在事务日志上。但是,其他一些数据源不是关系型数据库管理系统 (RDBMS),它们管理未提交事务的方式完全不同。只要事务回滚时,数据源可以撤消所有未提交的改变,那么这种技术应该可用于管理事务。

2、一致性( Consistency )

  事务在系统完整性中实施一致性,这通过保证系统的任何事务最后都处于有效状态来实现。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。因为事务开
始时系统处于一致状态,所以现在系统仍然处于一致状态。 再让我们回头看一下银行转帐的例子,在帐户转换和资金转移前,帐户处于有效状态。如果事务成功地完成,并且提交事务,则帐户处于新的有效的状态。如果事务出错,终止后,帐户返回到原先的有效状态。
记住,事务不负责实施数据完整性,而仅仅负责在事务提交或终止以后确保数据返回到一致状态。理解数据完整性规则并写代码实现完整性的重任通常落在 开发者肩上,他们根据业务要求进行设计。 当许多用户同时使用和修改同样的数据时,事务必须保持其数据的完整性和一致性。因此我们进一步研究ACID特性中的下一个特性:隔离性。

3、隔离性 ( Isolation)

  隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。事务的隔离性通过使用锁定来实现。

4、持久性 (Durabilily)

  持久性意味着一旦事务执行成功,在系统中产生的所有变化将是永久的。应该存在一些检查点防止在系统失败时丢失信息。甚至硬件本身失败,系统的状态仍能通过在日志中记录事务完成的任务进行重建。持久性的概念允许开发者认为不管系统以后发生了什么变化,完 成的事务是系统永久的部分。

  对于数据库来说,1、2、4特性较容易满足。数据库是一个共享资源,可以同时供多个用户使用。也就是需要对事务进行并发的控制,以满足隔离性。如果一个个事务是串行的执行,也就是一次只能执行一个事务,只有一个事务等到另外一个事务完全提交修改以后再执行另外一个事务,那么完全满足了隔离性。但是此时数据库系统大部分都是处于空闲状态,其效率将会很低。因此,事务应该允许并发的执行,并且应该对事务进行并发控制。如果不对事务进行并发控制,会出现以下三种情况:

1、丢失更新(Lost update)

  两个事务都同时更新一行数据,但是第二个事务却覆盖了第一个事务的修改。比如T1,T2事务都发现当前剩余量为16,然后减1, T1修改以后为15,T2修改以后同样为15,T2的修改覆盖了T1。

2、非重复读(Non-repeatable Reads)

  一个事务对同一行数据重复读取两次,但是却得到了不同的结果。同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。 更为通俗的说法是:在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。如果同一个事务在第二次读取的时候发现这条记录消失了或者增加了,这种情况叫做幻读。

3、脏读(Dirty Reads)

   一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交或者回滚,那么这个事务有可能读取的是修改前的值,导致了脏读。脏读和丢失更新的唯一区别就在于丢失更新所读取的数据是正确的。

  出现这样的原因是事务的隔离性遭到了破坏。但是在某些情况下,并发所造成的事务问题并不是完全不可接受的,比如有可能出现幻读。因此,在性能与事务特性的平衡选择下,SQL给出了4种隔离级别。SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

1.Read Uncommitted(读取未提交内容)

  在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

2.Read Committed(读取提交内容)

   这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

3.Repeatable Read(可重读)

  这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

4.Serializable(可串行化)

  这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

使用select @@tx_isolation;可以查看查看Mysql的默认的事务隔离级别:


mysql>    select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 rows in set (0.02 sec)

mysql>

  Mysql使用锁来完成并发控制。要特别说明的是,对于原子性,一致性和持久性来说,用户无法介入,但是对于并发控制,用户却能手工的灵活操纵,也就是说,数据库事务的隔离性有时候需要用户手工控制。
基本的封锁类型有三种种:排它锁(X锁)和共享锁(S锁)已经意向锁。

X锁

  所谓X锁,是事务T对数据A加上X锁时,只允许事务T读取和修改数据A,其他事务不能读取也不能修改数据库A。

S锁

  所谓S锁,是事务T对数据A加上S锁时,其他事务只能再对数据A加S锁,而不能加X锁,直到T释放A上的S锁。若事务T对数据对象A加了S锁,则T就可以对A进行读取,但不能进行更新(S锁因此又称为读锁),在T释放A上的S锁以前,其他事务可以再对A加S锁,但不能加X锁,从而可以读取A,但不能更新A。

意向锁:

  多粒度封锁协议允许多粒树中的每个结点被独立加锁。对一个结点加锁意味着这个结点的所有后裔结点也被加以同样类型的锁。显示封锁是数据库直接对数据对象加锁,隐式封锁是该对象没有被独立封锁,而是其上级对象被封锁而导致该数据对象被加锁。因此系统在对某一数据对象加锁时不仅要检查该数据对象上有无(显式和隐式)封锁与之冲突;还要检查其所有上级结点和所有下级结点,看申请的封锁是否与这些结点上的(显式和隐式)封锁冲突;显然,这样的检查方法效率很低。为此引进了意向锁。意向锁的含义是:对任一结点加锁时,必须先对它的上层结点加意向锁。

   例如事务T要对某个元组加X锁,则首先要对关系和数据库加IX锁。换言之,对关系和数据库加IX锁,表示它的后裔结点—某个元组拟(意向)加X锁。

   引进意向锁后,系统对某一数据对象加锁时不必逐个检查与下一级结点的封锁冲突了。例如,事务T要对关系R加X锁时,系统只要检查根结点数据库和R本身是否已加了不相容的锁(如发现已经加了IX,则与X冲突),而不再需要搜索和检查R中的每一个元组是否加了X锁或S锁。

锁定时间的长短

  锁保持的时间长度为保护所请求级别上的资源所需的时间长度。用于保护读取操作的共享锁的保持时间取决于事务隔离级别。采用 READ COMMITTED 的事务隔离级别时,只在读取页的期间内控制共享锁。在扫描中,直到在扫描内的下一页上获取锁时才释放锁。如果指定 HOLDLOCK 提示或者将事务隔离级别设置为 REPEATABLE READSERIALIZABLE,则直到事务结束才释放锁。

  根据为游标设置的并发选项,游标可以获取共享模式的滚动锁以保护提取。当需要滚动锁时,直到下一次提取或关闭游标(以先发生者为准)时才释放滚动锁。但是,如果指定 HOLDLOCK,则直到事务结束才释放滚动锁。

  请注意用于保护更新的排它锁(X锁)将直到事务结束才释放。

  如果一个连接试图获取一个锁,而该锁与另一个连接所控制的锁冲突,则试图获取锁的连接将一直阻塞到将冲突锁释放而且连接获取了所请求的锁或者连接的超时间隔已到期。默认情况下没有超时间隔,但是一些应用程序设置超时间隔以防止无限期等待。

存储引擎

  并不是所有的存储引擎都支持事务和锁,使用show engines查看各个存储引擎对事务和锁的概况:


mysql> show engines;
+------------+---------+------------------------------------------------------------+--------------+------+------------+
| Engine     | Support | Comment                                                    | Transactions | XA   | Savepoints |
+------------+---------+------------------------------------------------------------+--------------+------+------------+
| MRG_MYISAM | YES     | Collection of identical MyISAM tables                      | NO           | NO   | NO         |
| CSV        | YES     | CSV storage engine                                         | NO           | NO   | NO         |
| MyISAM     | DEFAULT | Default engine as of MySQL 3.23 with great performance     | NO           | NO   | NO         |
| InnoDB     | YES     | Supports transactions, row-level locking, and foreign keys | YES          | YES  | YES        |
| MEMORY     | YES     | Hash based, stored in memory, useful for temporary tables  | NO           | NO   | NO         |
+------------+---------+------------------------------------------------------------+--------------+------+------------+
5 rows in set (0.02 sec)

  可以发现,只有InnoDB才完全支持事务,行级锁和外键。MyISAM引擎不支持事务。如果要使用事务,必须是innodb引擎:alter table table_name engine=innodb;

分析Mysql事务中锁的变化

  假设有如下三条语句组成的事务


//1.查询出商品信息

select status from t_goods where id=1;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

  事务采用默认的MySQL默认的Repeatable Read隔离级别。如果要有关各种sql语句到底加什么锁的信息可以访问[dev.mysql.com/doc/refman/…]

select status from t_goods where id=1;   没有加锁,当前事务读,其他事务也可以读
insert 加排它锁,但不影响其他事务的插入其他记录。(排它锁,行级锁)
update 加排它锁,影响其他事务读这条记录(排它锁,行级锁)

这里只列举了Mysql手册中关于SELECT ... FROM 中锁的描述:
>
SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row.

可以看到,默认并没有加锁,那么有可能出现不可重复读的情况。只有在SERIALIZABLE的级别下,才会加shared next-key locks。

  用户可以为select手工的加上一些锁,以防止其他事务访问。比如select for update


//1.查询出商品信息

select status from t_goods where id=1 for update;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

  上面我们提到,使用select…for update会把数据给锁住,加上一个排它锁。不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。select…for update 的排它锁,只有等到事务结束才释放。
有关select for update的更多信息,可以访问:dev.mysql.com/doc/refman/… 了解更多。

事务边界

  Mysql默认自动提交事务,因此一条SQL语句就是一个事务。可以使用show variables like 'autocommit'查看事务自动提交状态:

show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 rows in set (0.03 sec)

  如果一个事务要包含多条事务,那么必须显示指定事务的边界。比如使用 begincommit关键字。

Mysql中的锁

排它锁,读锁和意向锁只是从锁的作用来划分。从锁的粒度来划分,Mysql锁的类型可以从dev.mysql.com/doc/refman/… 了解到。请注意,行级锁,表级锁,页级锁只是一种级别划分。并不存在这样的锁的名字叫做“行级锁,表级锁,页级锁”。

查看表的基本状态(存储引擎类型,索引数等):

SHOW TABLE STATUS FROM database_name;

JDBC中的事务与Mysql中的事务

从JDBC的角度来说,提交事务就是提交sql给数据库来执行。由数据库来保证最后的事务处理。JDBC的事务其实就是保证这些事务之间的数据要么全部交给数据库执行,要么全部不交给数据库。如果设置了自动提交,那么sql一旦出现,就立即执行。 MYSQL默认是自动提交的,也就是你提交一个QUERY,它就直接执行!我们可以通过
set autocommit=0 禁止自动提交
set autocommit=1 开启自动提交
所以,在JDBC中,一起提交sql语句才起作用。(JDBC会给数据库增加事务处理语句,然后再发给数据库)
因此,mysql是否开启自动提交关系不大。
如果事务不是由jdbc来生成的,那么提交一系列的sql语句到数据库以后就被数据库当做单条数据库执行了,那么就没有意义了。