为什么要加锁
多核计算机的出现,计算机实现真正并行计算,可以在同一时刻,执行多个任务。在多线程编程中,因为线程执行顺序不可控导致的数据错误。比如,多线程的理想状态是这样的
但是实际情况是这样的:在网络编程中,在同一时刻,多个客户端同时请求同一个资源,如果不做控制,也会带来数据错误。比如在同一时间有10000人去抢10张火车票,10张火车票有可能会买给100个人,这显然是不符合要求的。 在多线程编程中,为了解决线程执行不可控带来的问题,通常情况下都是通过加锁来实现数据同步的。在网络编程中,也可以通过加锁机制来控制。
在网络编程中,可以通过给数据库加锁,达到控制并发的目的。在php开发时,基本都是使用mysql作为数据库。所以,就会给mysql加锁控制网络并发引起数据错误问题。
MySQL的存储引擎
不是要说MySQL的锁吗,怎么说上存储引擎了?因为MySQL存储引擎不同,锁也会不同。MySQL有MyISAM 和InnoDB两种存储引擎,现在主要使用InnoDB,所以主要介绍InnoDB下锁的使用。
InnoDB引擎支持事务操作,使用事务可以保证多条sql语句执行的完整性(要不都成功,要不都失败)
事务是由一组SQL语句组成的逻辑处理单元,事务具有4属性
- 原子性(Actomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以操持完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
- 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
多个事务并发执行会带来新的问题
- 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个 事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改保存其更改副本的编辑人员覆盖另一个编辑人员所做的修改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题
- 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做“脏读”。
- 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。
- 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
曾经年少无知的我,以为使用事务就能保证并发情况下数据同步问题,后来的一次惨痛经历才明白了,事务不能保证并发情况的数据同步问题,需要事务和锁同时使用才能保证。
锁的种类
- 乐观锁 机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。相对悲观锁而言,乐观锁更倾向于开发运用。
- 悲观锁 具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
MySQL中锁的种类
乐观锁和悲观锁是一种思想,不是具体实现,在MySQL中,有锁的具体的实现方式 (文中的线程在MySQL中可以视作MySQL的连接)
- 共享锁 一个线程在持有锁时,其他的线程可以查询被锁的数据,但是不能修改,不能删除。实现方式
SELECT * FROM table_name WHERE id =? lock in share mode;
- 排它锁 一个线程在持有锁时,其他的线程不能查询,不能更新,不能删除被锁的数据,直到锁被释放. >
SELECT * FROM table_name WHERE id =? for update
- 总结一下:共享锁类似于java中的读锁,一个线程在持有乐观锁的时候,其他的线程也可以对被锁的数据进行读操作,但是不能对被锁的数据进行删除和更新操作;排他锁类似于java的写锁,一个线程持有写锁的时候,其他的线程不能再对被锁的数据进行任何查询,更新,删除操作。
- 重点 InnoDB的行锁是基于索引实现的,如果在查询中不使用索引,会锁表。
MySQL锁粒度
- 表级锁 是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁分为表共享读锁与表独占写锁。
- 行级锁 是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
共享锁的使用
注意: 下面的操作,都是行锁操作,MySQL为InnoDB引擎,id为自增主键
先创建一个测试表
1CREATE TABLE `test` (
2 `id` int(0) NOT NULL AUTO_INCREMENT,
3 `name` varchar(20) NOT NULL,
4 `number` bigint(0) NOT NULL,
5 `age` int(0) NULL,
6 PRIMARY KEY (`id`)
7) ENGINE = InnoDB;
看一下随便插入的几条数据
没有问题,使用共享锁看一下效果:
- 开启事务
begin;
- 给id为1的数据加共享锁
mysql> select * from test where id=1 lock in share mode;
- 分别使用加锁和不加锁查询id为1 的数据>都可以查询到数据
修改id为1 的数据看看
sql语句会一直停在这里,直到超时或者锁释放(事务提交或者回滚)左边的事务提交后,右边的sql会执行,完成更新操作。
同样,删除操作也会等待锁释放才能操作,这里就不演示了。
再看一下另一种情况,左边锁住id为5的数据,右边更新id为1 的数据,不受影响。这就是行级锁,只会锁住相关的一行数据
排他锁的使用
还是使用test这张表
- 开启事务
begin;
- 给id1的数据加排他锁
select * from test where id = 1 for update;
- 在右边查询id为1的数据查询语句会一直等待,直到超时或者锁释放(左边commit或者rollback)
左边commit后
使用排它锁对id为5的数据加锁后,更新id为5的数据
sql语句同样会等待,直到超时或者锁释放,删除操作也是一样
看一下对id为1的数据加锁,然后操作id不为1的数据的情况
没有问题,MySQL只是锁住了id为5的数据,其他的数据都可以操作。
看一下InnoDB引擎锁表的情况
我们常常说InnoDB是行锁,但是这里介绍一下它锁表的情况。因为name列没有索引,所以,在加行锁的时候,MySQL不能加正常加行锁,会锁住整张表。 InnoDB行锁是通过索引上的索引项来实现的,这一点MySQL与Oracle不同,后者是通过在数据中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味者:只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁! 在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
再看另一种情况
使用排它锁对id为1的数据加锁时,使用不加锁的查询和没有约束的查询时,一样可以立刻查询到数据。只有使用加锁的查询或者更新和删除时才会等待锁释放。
总结
- InnoDB的锁配合事务使用
- MySQL有共享锁和排它锁
- 使用共享锁时,其他线程(连接)可以查询数据,但是不能更新和删除数据,使用排它锁时,不能查询数据不能更新数据,不能删除数据
- MySQL的InnoDB引擎支持行级锁和表级锁,行级锁
- InnoDB的行级锁是基于索引的,加锁是对索引加锁,加锁时没有索引时会锁住整张表
以上是我对MySQL锁的理解,文中如果有不正确的地方,还请各位大哥批评指正。