令人惊讶的是,大部分数据库系统都没有提供真正的隔离性,最初或许是因为系统实现者并没有真正理解这些问题。如今这些问题已经弄清楚了,但是数据库实现者在正确性和性能之间做了妥协。ISO和 ANIS SQL标准制定了四种事务隔离级别的标准,但是很少有数据库厂商遵循这些标准。比如 Oracle数据库就不支持READ UNCOMMITTED和 REPEATABLE READ的事务隔离级别。
SQL标准定义的四个隔离级别为:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
READ UNCOMMITTED称为浏览访问( browse access,仅仅针对事务顶的READ COMMITTED称为游标稳定( cursor stability)。 REPEATABLE READ是2.9999°的隔离,没有幻读的保护。 SERIALIZABLE称为隔离,或3°的隔离。SQL和SQL2标准的默认事务隔离级别是 SERIALIZABLE。
InnoDB存储引擎默认支持的隔离级别是 REPEATABLE READ,但是与标准SQL不同的是, InnoDB存储引擎在 REPEATABLE READ事务隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生。这与其他数据库系统(如 Microsoft SQL Server数据库〕是不同的。所以说, InnoDB存储引擎在默认的 REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的 SERIALIZABLE隔离级别。
隔离级别越低,事务请求的锁越少或保持锁的时间就越短。这也是为什么大多数数据库系统默认的事务隔离级别是READ COMMITTED。
据了解,大部分的用户质疑 SERIALIZABLE隔离级别带来的性能问题,但是根据 Jim gray在《 Transaction Processing》一书中指出,两者的开销几乎是一样的,甚至SERIALIZABLE可能更优!因此在 InnoDB存储引擎中选择 REPEATABLE READ的事务隔离级别并不会有任何性能的损失。同样地,即使使用 READ COMMITTED的隔离级别,用户也不会得到性能的大幅度提升。
在 InnoDB存储引擎中,可以使用以下命令来设置当前会话或全局的事务隔离级别:
SET [GLOBAL I SESSION] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }复制代码
如果想在 MySQL数据库启动时就设置事务的默认隔离级别,那就需要修改MyQL的配置文件,在[mysqld]中添加如下行:
[mysqld] transaction-isolation=READ-COMMITTED复制代码
查看当前会话的事务隔离级别,可以使用:
mysql> SELECT @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.05 sec)复制代码
查看全局的事务隔离级别,可以使用
mysql> SELECT @@global.tx_isolation; +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row in set, 1 warning (0.00 sec)复制代码
在SERIALIABLE的事务隔离级别, InnoDB存储引擎会对每个 SELECT语句后自动加上 LOCK IN SHARE MODE,即为每个读取操作加一个共享锁。因此在这个事务隔离级别下,读占用了锁,对一致性的非锁定读不再予以支持。这时,事务隔离级别 SERIALIZABLE符合数据库理论上的要求,即事务是well-formed的,并且是two-phrased的。有兴趣的读者可进一步研究因为 InnoDB存储引擎在 REPEATABLE READ隔离级别下就可以达到3°的隔离,因此一般不在本地事务中使用 SERIALIABLE的隔离级别。 SERIALIABLE的事务隔离级别主要用于 InnoDB存储引擎的分布式事务。
在 READ COMMITTED的事务隔离级别下,除了唯一性的约束检查及外键约束的检查需要 gap lock, InnoDB存储引擎不会使用gap lock的锁算法。但是使用这个事务隔离级别需要注意一些问题。首先,在 MySQL5.1中, READ COMMITTED事务隔离级别默认只能工作在 replication(复制)二进制日志为ROW的格式下。如果二进制日志工作在默认的 STATEMENT下,则会出现如下的错误:
ERROR 1598(HY000): Binary logging not possible. Message: Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT复制代码
在 MySQL5.0版本以前,在不支持ROW格式的二进制日志时,也许有人知道通过将参数 innodb_locks_unsafe_for_binlog设置为1可以在二进制日志为 STATEMENT下使用 READ COMMITTED的事务隔离级别:
可以看到,数据产生了不一致。导致这个问题发生的原因有两点:
- 在READ COMMITTED事务隔离级别下,事务没有使用 gap lock进行锁定,因此用户在会话B中可以在小于等于5的范围内插入一条记录;
- STATEMENT格式记录的是 master上产生的SQL语句,因此在 master服务器上执行的顺序为先删后插,但是在 STATEMENT格式中记录的却是先插后删,逻辑顺序上产生了不一致。
要避免主从不一致的问题,只需解决上述问题中的一个就能保证数据的同步了。使用 READ REPEATABLE的事务隔离级别可以避免上述第一种情况的发生,也就避免了 master和 slave数据不一致问题的产生。
在 MySQL5.1版本之后,因为支持了ROW格式的二进制日志记录格式,避免了第二种情况的发生,所以可以放心使用READ COMMITTED的事务隔离级别。但即使不使用READ COMMITTED的事务隔离级别,也应该考虑将二进制日志的格式更换成ROW,因为这个格式记录的是行的变更,而不是简单的SQL语句,所以可以避免一些不同步现象的产生,进一步保证数据的同步。InnoDB存储引擎的创始人Heikkituuri也在http://bugs.mysql.com/bug.php?id=33210这个帖子中建议使用ROW格式的二进制日志。