这有一把钥匙,打开mysql的锁

前言

锁是计算机协调多个进程或线程并发访问某一资源的控制机制。在数据库中,除了计算资源(CPU、I/O)的争用以外,数据也是一种多用户共享的资源。如何保证数据并发访问的一致性和有效性是全部数据库必须解决的一个问题。相对于其余数据库而言,mysql的锁机制比较简单,最显著的特色是不一样的存储引擎支持不一样的锁机制。好比MyISAM和MEMORY采用表级锁;BDB存储引擎采用页面锁,但也支持表级锁;InnoDB存储引擎采用行级锁,也支持表级锁,默认状况使用行级锁。三种锁的特色以下:mysql

  • 表级锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突的几率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的几率最低,并发度也高。
  • 页面锁:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁粒度介于表锁和行锁之间,并发度通常。

表锁

Mysql表锁有两种模式:表共享读锁(table read lock)和表独占写锁(table write lock)。对表的读操做不会阻塞其余用户对同一表的读操做,但会阻塞对同一表的写请求;对表的写操做,会阻塞其余用户对同一表的读和写操做。
准备数据:sql

CREATE TABLE `test` (
  `id` bigint(20) NOT NULL auto_increment,
  `name` varchar(100) NOT NULL,
  `age` int(10) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='测试表';
复制代码

表独占写锁

一、用户1:lock table test write; 给test表加独占写锁 二、当前用户1对test表的read,update和insert均可以执行 数据库

用户2对test表的查询被阻塞,须要等待用户1对test表的写锁释放

三、用户1释放test表的写锁

用户2马上查询到结果

表共享读锁

一、用户1给test表加共享读锁bash

二、当前用户1能够查询test表的数据,用户2也能够查询test表的数据

三、用户1对test表进行更新,插入报错,用户2对test表的更新和插入会等待锁

用户1更新报错: 并发

用户2更新会等待锁释放:
四、用户1查询其余表也会报错,用户2能够查询其余表

用户1查询其余表报错: oracle

五、用户1释放表读锁,用户2等到了锁释放,更新完成

查询表级锁竞争状况

能够经过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺:若是table_lock_waited的值比较高,则说明存在较严重的表级锁争用状况。测试

并发插入

上面提到MyISAM表的读和写是串行的,但这是就整体而言。在必定条件下,MyISAM也支持查询和插入的并发执行。MyISAM存储引擎有一个系统变量concurrent_insert,专门用来控制并发插入的行为,能够取值0,1,2;优化

  • 当concurrent_insert=0,不容许并发插入。
  • 当concurrent_insert=1,若是MyISAM表中没有空洞(即表的中间没有被删除的行),容许一个进程在查询数据,另外一个进程在表尾插入记录,这也是mysql的默认设置。
  • 当concurrent_insert=2,不管MyISAM表有没有空洞,均可以在表尾插入记录。

举例:
一、用户1对test表加共享读锁(注意这里要用read local)ui

二、用户2能够对test表进行插入操做,可是更新会等待锁释放

可是即便用户2插入成功,可是用户1仍是只能查到加锁以前的记录,查询不到用户2新插入的记录spa

InnoDb行锁

两种行锁

  • 共享锁(s):又称读锁。容许一个事务去读一行,阻止其余事务得到相同数据集的排它锁。若事务1对记录A加上S锁,则事务1能够读取A但不能修改A,其余事务只能再对A加S锁,不能加X锁。这就保证了其余事务能够读A,但在事务1释放A上的S锁以前不能对A作任何的修改。
  • 排他锁(X):又称写锁。获取排他锁的事务能够更新数据,阻止其余事务获取相同数据集的共享读锁和排他写锁。若事务1对记录A加了X锁,则事务1能够读A,也能够修改A,其余事务不能对A加任何锁,知道事务1释放A上的锁。
    注意:对于共享锁很好理解,就是多个事务只能读取数据不能修改数据。可是排他锁容易错误地理解成:若是一个事务锁住一行数据后,其余事务不能读物和修改该行数据。其实排他锁指的是一个事务对一条记录加上排他锁,其余事务不能对该记录加其余的锁。innoDb引擎默认的update,delete和insert会自动给涉及的数据加上排他锁,select语句默认不会加任何锁。因此加过排他锁的数据行在其余事务中不能修改数据,也不能经过for update加排他锁或者lock in share mode加共享锁,可是能够直接经过select...from...的方式查询数据,由于普通的查询没有任何锁机制。
    事务能够经过如下语句显式给记录集加共享锁或排他锁:
  • 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
  • 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。

行锁实现方式

InnoDb行锁是经过给索引上的索引项加锁来实现的,这一点mysql与oracle不一样,oracle是经过在数据块中对相应数据行加锁来实现的。InnoDb的这种行锁实现特色意味着:只有经过索引条件检索数据,InnoDb才会使用行级锁,不然InnoDb将使用表锁。 举个例子:

  • 在不经过索引条件查询的时候,InnoDb使用的是表锁,而不是行锁。
create table test_no_index
(
	id bigint(20) not null,
	name varchar(50) not null 
) ENGINE = INNODB COMMENT ='测试innoDb的行锁实现方式';

insert into test_no_index(id,name) values(1,'1');
insert into test_no_index(id,name) values(2,'2');
复制代码

一、当用户1查询id=1的记录并对这行记录加上排他锁(要把自动提交关掉)

二、这时用户2查询id=2的记录却须要等待。缘由是在没有索引的状况下,InnoDb只能使用表锁。

三、当咱们给这个表的id字段加上一个索引后,再重复执行第1,2步骤

alter table test_no_index add index id(id);
复制代码

用户1成功锁定id=1的记录

用户2也能够成功查询并锁定id=2的记录

间隙锁(Next-Key锁)

当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDb会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但不存在的记录,叫作间隙(GAP),InnoDb也会对这个“间隙”加锁,这种机制就是所谓的“间隙锁”。
例如:假如emp表中有101条数据,id的值分别是1,2,...,100,101,下面的sql:

select * from emp where id>100 for update;
复制代码

是一个范围条件的检索,InnnoDb不只会对符合条件的id=101的记录加锁,也会对id>101的间隙加锁。

InnoDb使用间隙锁的目的,一方面是为了防止幻读,以知足隔离级别的要求。对于上面的例子,要是不使用间隙锁,若是其余事务插入了id>100的任何记录,那么本事务若是再次执行上述语句,就会产生幻读。另外一方面,是为了知足其恢复和复制的须要。
显然,在使用范围条件检索并锁定记录时,InnoDb这种锁机制会阻塞符合条件范围内键值的并发插入,这每每会形成严重的锁等待。所以,在实际应用开发中,尤为是并发插入比较多的应用,要尽可能优化业务逻辑,尽可能使用相等来访问和更新数据,尽可能避免使用范围条件。

悲观锁和乐观锁

首先,悲观锁和乐观锁都是一种思想,并非真实存在于数据中的一种机制。

悲观锁

当认为数据被并发修改的概率比较大,须要在修改以前借助于数据库的锁机制对数据进行加锁的思想称为悲观锁,又称PCC(Pessimisic Concurrency Control)。在效率方面,处理锁的操做会产生额外的开销,并且增长了死锁的机会。当一个线程在处理某行数据的时候,其它线程只能等待。 ** 实现方式 ** 悲观锁的实现依赖数据库的锁机制,流程以下:

  • 修改记录前,对记录加上排他锁。
  • 若是加锁失败,说明这条记录正在被修改,那么当前查询要等待会抛出异常。
  • 若是加锁成功,能够对这条记录进行修改,事务完成后进行解锁。
  • 加锁修改期间,其余事务想要对这条记录进行操做,都要等待锁释放或者不想等待抛出异常。 注意:在使用innoDb引擎实现悲观锁时,必须关闭自动提交。举例:
set autocommit=0;
//事务开始
begin;
//查询商品在某个仓库的库存
select * from inventory_summary where sku_id=1000 and warehouse_id=123456 for update;
//修改商品库存为2
update inventory_summary set quantity=2 where sku_id=1000 and warehouse_id=123456;
//提交事务
commit;
复制代码

乐观锁

乐观锁的实现不须要借助数据库的锁机制,只要两个步骤:冲突检测(比较)和数据更新,其中一种典型的实现方法就是CAS(比较交换,Compare And Swap)。CAS这里不作详细解释,就是先比较后更新,在对一个数据进行更新前,先持有这个数据原有值得备份,若是当前更新的值和原来的备份相等才进行更新,不然判断为数据被其余线程改过了。当前线程再次进行重试。 *** ABA问题 ***

//查询出商品的库存是3,而后用库存为3做为条件进行更新
select quantity from inventory_summary where sku_id=1000 and warehouse_id=123456;
//修改库存为2
update inventory_summary set quantity=2 where sku_id=1000 and warehouse_id=123456 and quantity=3;
复制代码

在更新以前,先查询原有的库存数,在更新库存时,用原有的库存数做为修改条件。相等则更新,不然认为是被改过的数据。可是会存在这样的状况下,好比线程A取出库存数3,线程B先将库存数更新为2,又将库存数更新为3,而后线程A更新的时候发现库存仍然是3,而后更新成功。可是这个过程可能存在问题。** 解决ABA问题一个方法是经过一个顺序递增的version字段或者时间戳**

//查询商品库存表,version=1
select version from inventory_summary where sku_id=1000 and warehouse_id=123456;

//修改商品库存为2
update inventory_summary set quantity=2,version=version+1 where sku_id=1000 and warehouse_id=123456 and version=1;
复制代码

在每次更新的时候都带上一个版本号,一旦版本号和数据版本号一致就能够执行修改并对版本号执行+1操做,不然执行失败。由于每次操做版本号递增,因此不会出现ABA问题。还可使用时间戳,由于时间戳具备自然的顺序递增性。

乐观锁和悲观锁比较

乐观锁并非真正的加锁,优势是效率高,缺点是更新时间的几率比较高(尤为是并发度比较高的环境中);悲观锁依赖于数据库锁机制,更新失败的几率低,可是效率也低。

相关文章
相关标签/搜索