MySQL InnoDB锁机制

概述:

  锁机制在程序中是最经常使用的机制之一,当一个程序须要多线程并行访问同一资源时,为了不一致性问题,一般采用锁机制来处理。在数据库的操做中也有相同的问题,当两个线程同时对一条数据进行操做,为了保证数据的一致性,就须要数据库的锁机制。每种数据库的锁机制都本身的实现方式,mysql做为一款工做中常常遇到的数据库,它的锁机制在面试中也常常会被问到。因此本文针对mysql数据库,对其锁机制进行总结。html

  mysql的锁能够分为服务层实现的锁,例如Lock Tables、全局读锁、命名锁、字符锁,或者存储引擎的锁,例如行级锁。InnoDB做为MySQL中最为常见的存储引擎,本文默认MySQL选择InnoDB做为存储引擎,将MySQL的锁和InnoDB实现的锁同时进行讨论。java

  锁的分类按照特性有多种分类,常见的好比显式锁和隐式锁;表锁和行锁;共享锁和排他锁;乐观锁和悲观锁等等,后续会在下方补充概念。mysql

服务级别锁:

  表锁面试

  表锁能够是显式也能够是隐式的。显示的锁用Lock Table来建立,但要记得Lock Table以后进行操做,须要在操做结束后,使用UnLock来释放锁。Lock Tables有read和write两种,Lock Tables......Read一般被称为共享锁或者读锁,读锁或者共享锁,是互相不阻塞的,多个用户能够同一时间使用共享锁互相不阻塞。Lock Table......write一般被称为排他锁或者写锁,写锁或者排他锁会阻塞其余的读锁或者写锁,确保在给定时间里,只有一个用户执行写入,防止其余用户读取正在写入的同一资源。算法

  为了进行测试,咱们先建立两张测试表,顺便加几条数据sql

CREATE TABLE `test_product` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `code` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `quantity` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

CREATE TABLE `test_user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(3) DEFAULT NULL,
  `gender` int(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

INSERT INTO `test_user` (`id`, `name`, `age`, `gender`) VALUES ('1', '张三', '16', '1');
INSERT INTO `test_user` (`id`, `name`, `age`, `gender`) VALUES ('2', '李四', '18', '1');
INSERT INTO `test_product` (`id`, `code`, `name`, `price`, `quantity`) VALUES ('1', 'S001', '产品1号', '100.00', '200');
INSERT INTO `test_product` (`id`, `code`, `name`, `price`, `quantity`) VALUES ('2', 'S001', '产品2号', '200.00', '200');
INSERT INTO `locktest`.`test_product` (`code`, `name`, `price`, `quantity`) VALUES ('S003', '产品3号', '300.00', 300);
INSERT INTO `locktest`.`test_product` (`code`, `name`, `price`, `quantity`) VALUES ('S004', '产品4号', '400.00', 400);
INSERT INTO `locktest`.`test_product` (`code`, `name`, `price`, `quantity`) VALUES ('S005', '产品5号', '500.00', 500);

 

  打开两个客户端链接A和B,在A中输入数据库

  LOCK TABLES test_product READ;

  在B中输入安全

  SELECT * FROM test_product

  B能正常查询并获取到结果。Lock Tables....Read不会阻塞其余线程对表数据的读取。 服务器

  让A继续保留锁,在B中输入多线程

update test_product set price=250 where id=2;

  此时B的线程被阻塞,等待A释放锁。

 

  释放A持有的锁,在A中输入

UNLOCK TABLES;

  此时B中显示下图,而且数据已经被变动。

  Lock Tables....Read会阻塞其余线程对数据变动

 

  接下来再对Lock Table....write进行测试,在A线程下执行如下语句,用排它锁锁定test_product。

LOCK TABLES test_product  WRITE;

 

  在B中输入如下语句,对test_product表进行查询。

SELECT * FROM test_product;

  发现B的查询语句阻塞,等待A释放锁。再开启一个新命令窗口C,输入

update test_product set price=250 where id=2;

  一样被阻塞。在A中使用UNLOCK释放锁,B、C成功执行。Lock Tables....Write会阻塞其余线程对数据读和写。

  

  假设在A中进行给test_product加读锁后,对test_product进行更新或者对test_user进行读取更新会怎么样呢。

  LOCK TABLES test_product READ;

  以后在A中进行test_product更新

update test_product set price=250 where id=2;

[SQL]update test_product set price=250 where id=2;
[Err] 1099 - Table 'test_product' was locked with a READ lock and can't be updated

  而后在A中读取test_user

[SQL]SELECT * from test_user

[Err] 1100 - Table 'test_user' was not locked with LOCK TABLES

  Lock Tables....Read不容许对表进行更新操做(新增、删除也不行),而且不容许访问未被锁住的表。

  

  对Lock Table....WRITE进行相同的实验,代码类似,就再也不贴出。

  Lock Tables....WRITE容许对被锁住的表进行增删改查,但不容许对其余表进行访问。

  总结上面的结论

  1. Lock Tables....READ不会阻塞其余线程对表数据的读取,会阻塞其余线程对数据变动
  2. Lock Tables....WRITE会阻塞其余线程对数据读和写
  3. Lock Tables....READ不容许对表进行更新操做(新增、删除也不行),而且不容许访问未被锁住的表
  4. Lock Tables....WRITE容许对被锁住的表进行增删改查,但不容许对其余表进行访问

 

  lock tables主要性质如上所述,当咱们要去查询mysql是否存在lock tables锁状态能够用下面语句进行查询。第二条能够直接看到被锁的表。也能够经过show process来查看部分信息。

LOCK TABLES test_product  READ,test_user WRITE;

show status like "%lock%";

show OPEN TABLES where In_use > 0;

  

       

  使用LOCK TABLES时候必须当心,《高性能MySQL》中有一段话:

  LOCK TABLES和事务之间相互影响的话,状况会变得很是复杂,在某些MySQL版本中甚至会产生没法预料的结果。所以,本书建议,除了事务中禁用了AUTOCOMMIT,可使用LOCK TABLES以外,其余任什么时候候都不要显示地执行LOCK TABLES,无论使用什么存储引擎。

  因此在大部分时候,咱们不须要使用到LOCK TABLE关键字。

  

  

  全局读锁

  全局锁能够经过FLUSH TABLES WITH READ LOCK获取单个全局读锁,与任务表锁都冲突。解锁的方式也是UNLOCK TABLES。一样设置A、B两个命令窗口,咱们对全局锁进行测试。

  在A中获取全局读锁

FLUSH TABLES WITH READ LOCK;

 

  而后在A窗口依次作如下实验

 

1 LOCK TABLES test_user READ;
2 
3 LOCK TABLES test_user WRITE;
4 
5 SELECT * from test_user;
6 
7 update test_product set price=250 where id=1;

  第一、5行可以执行成功,第二、7行执行会失败

 

  在B中执行

1 FLUSH TABLES WITH READ LOCK;
2 
3 LOCK TABLES test_user READ;
4 
5 LOCK TABLES test_user WRITE;
6 
7 SELECT * FROM test_product;
8 
9 update test_product set price=250 where id=2;

  B窗口中执行一、三、7成功。执行五、9失败。

  全局读锁其实就至关于用读锁同时锁住全部表。若是当前线程拥有某个表的写锁,则获取全局写锁的时候会报错。若是其余线程拥有某张表的写锁,则全局读锁会阻塞等待其余表释放写锁。

  该命令是比较重量级的命令,会阻塞一切更新操做(表的增删改和数据的增删改),主要用于数据库备份的时候获取一致性数据。

 

  命名锁

  命名锁是一种表锁,服务器建立或者删除表的时候会建立一个命名锁。若是一个线程LOCK TABLES,另外一个线程对被锁定的表进行重命名,查询会被挂起,经过show open tables能够看到两个名字(新名字和旧名字都被锁住了)。

 

  字符锁

  字符锁是一种自定义锁,经过SELECT GET_LOCK("xxx",60)来加锁 ,经过release_lock()解锁。假设A线程执行get_lock("xxx",60)后执行sql语句返回结果为1表示拿到锁,B线程一样经过get_lock("xxx",60)获取相同的字符锁,则B线程会处理阻塞等待的情况,若是60秒内A线程没有将锁释放,B线程获取锁超时就会返回0,表示未拿到锁。使用get_lock()方法获取锁,若是线程A调用了两次get_lock(),释放锁的时候也须要使用两次release_lock()来进行解锁。

 

InnoDB锁:

  InnoDB存储引擎在也实现了本身的数据库锁。通常谈到InnoDB锁的时候,首先想到的都是行锁,行锁相比表锁有一些优势,行锁比表锁有更小锁粒度,能够更大的支持并发。可是加锁动做也是须要额外开销的,好比得到锁、检查锁、释放锁等操做都是须要耗费系统资源。若是系统在锁操做上浪费了太多时间,系统的性能就会受到比较大的影响。

  InnoDB实现的行锁有共享锁(S)排它锁(X)两种

  共享锁:容许事务去读一行,阻止其余事务对该数据进行修改

  排它锁:容许事务去读取更新数据,阻止其余事务对数据进行查询或者修改

 

  行锁虽然很赞,可是还有一个问题,若是一个事务对一张表的某条数据进行加锁,这个时候若是有另一个线程想要用LOCK TABLES进行锁表,这时候数据库要怎么知道哪张表的哪条数据被加了锁,一张张表一条条数据去遍历是不可行的。InnoDB考虑到这种状况,设计出另一组锁,意向共享锁(IS)意向排他锁(IX)。

  意向共享锁:当一个事务要给一条数据加S锁的时候,会先对数据所在的表先加上IS锁,成功后才能加上S锁

  意向排它锁:当一个事务要给一条数据加X锁的时候,会先对数据所在的表先加上IX锁,成功后才能加上X锁

  意向锁之间兼容,不会阻塞。可是会跟S锁和X锁冲突,冲突的方式跟读写锁相同。例如当一张表上已经有一个排它锁(X锁),此时若是另一个线程要对该表加意向锁,无论意向共享锁仍是意向排他锁都不会成功。

线程 A 线程 B

BEGIN;

 

SELECT * FROM test_product for UPDATE;

 

 

SELECT * FROM test_product LOCK IN SHARE MODE;   

结果:线程阻塞

 

SELECT * FROM test_product for UPDATE;

结果:线程阻塞

COMMIT;  

 

  上面的例子中,用的两个加锁方式,一个是SELECT........FOR UPDATE,SELECT........LOCK IN SHARE MODE。SELECT FOR UPDATE能为数据添加排他锁,LOCK IN SHARE MODE为数据添加共享锁。这两种锁,在事务中生效,而当事务提交或者回滚的时候,会自动释放锁。遗憾的是,当咱们在项目中遇到锁等待的时候,并无办法知道是哪一个线程正在持有锁,也很难肯定是哪一个事务致使问题。可是咱们能够经过这几个表来确认消息Information_schema.processList、Information_schema.innodb_lock_waits、Information_schema.innodb_trx、Information_schema.innodb_locks来获取事务等待的情况,根据片面的锁等待情况来获取具体的数据库信息。

 

  隐式加锁:SELECT FOR UPDATE和LOCK IN SHARE 这种经过编写在mysql里面的方式对须要保护的数据进行加锁的方式称为是显式加锁。还有一种加锁方式是隐式加锁,除了把事务设置成串行时,会对SELECT到的全部数据加锁外,SELECT不会对数据加锁(依赖于MVCC)。当执行update、delete、insert的时候会对数据进行加排它锁。

  

  自增加锁:mysql数据库在不少时候都会设置为主键自增,若是这个时候使用表锁,当事务比较大的时候,会对性能形成比较大的影响。mysql提供了inodb_atuoinc_lock_mode来处理自增加的安全问题。该参数能够设置为0(插入完成以后,即便事务没结束也当即释放锁)、1(在判断出自增加须要使用的数字后就当即释放锁,事务回滚也会形成主键不连续)、2(来一个记录就分配一个值,不使用锁,性能很好,可是可能致使主键不连续)。

 

  外键锁: 当插入和更新子表的时候,首先须要检查父表中的记录,并对附表加一条lock in share mode,而这可能会对两张表的数据检索形成阻塞。因此通常生产数据库上不建议使用外键。

  索引和锁:InnoDB在给行添加锁的时候,实际上是经过索引来添加锁,若是查询并无用到索引,就会使用表锁。作个测试

  

线程 A 线程 B

set autocommit=0;

BEGIN;
Select * from test_product where price= 300 for UPDATE;

 

 

 

set autocommit=0;


BEGIN;
Select * from test_product where price=400 for UPDATE;

线程阻塞

COMMIT;  

 

     

  如上所示,若是正常锁行的话,两条线程锁住不一样行,不该该有冲突。咱们如今给price添加索引再试一次。     

ALTER TABLE `test_product` ADD INDEX idx_price ( `price` );

    

线程 A 线程 B

set autocommit=0;

BEGIN; 
Select * from test_product where price= 300 for UPDATE;

 

 

set autocommit=0;


BEGIN; 
Select * from test_product where price=400 for UPDATE;

 

Select * from test_product where price= 300 for UPDATE;

  阻塞

   

  添加索引之后会发现,线程A、B查询不一样的行的时候,两个线程并无相互阻塞。可是,即便InnoDB中已经使用了索引,仍然有可能锁住一些不须要的数据。若是不能使用索引查找,InnoDB将会锁住全部行。由于InnoDB中用索引来锁行的方式比较复杂,其中牵涉到InnoDB的锁算法和事务级别,这个后续会讲到。

  《高性能MySQL》中有一句话:"InnoDB在二级索引上使用共享锁,但访问主键索引须要排他锁,这消除了覆盖索引的可能性,而且使得SELECT FOR UPDATE 比Lock IN SHARE LOCK 或非锁定查询要慢不少"。除了上面那句话还有一句话有必要斟酌,"select for update,lock in share mode这两个提示会致使某些优化器没法使用,好比覆盖索引,这些锁定常常会被滥用,很容易形成服务器的锁争用问题,实际上应该尽可能避免使用这两个提示,一般都有更好的方式能够实现一样的目的。

  

锁算法和隔离级别:

锁算法:InnoDB的行锁的算法为如下三种

  Record Lock:单挑记录上的锁

  Gap Lock:间隙锁,锁定一个范围,但不包括记录自己

  Next-Key Lock:Record Lock+Gap Lock,锁定一个范围,而且锁定记录自己

  InnoDB会根据不一样的事务隔离级别来使用不一样的算法。网上关于InnoDB不一样的事务隔离级别下的锁的观点各不一致,有些甚至和MVCC混淆,这一块有时间再进行整理。能够去官网上详细了解一下,Mysql官网对InnoDB的事务锁的介绍

  MVCC:多版本控制,InnoDB实现MVCC是经过在每行记录后面保存两个隐藏的列来实现,一个保存建立的事务版本号,一个保存的是删除的事务版本号。MVCC只有在REPEATABLE READ 和 READ COMMITED两个隔离级别下工做。另外两个隔离级别与MVCC并不兼容,由于READ UNCOMMITED老是读取最新数据,跟事务版本无关,而SERIALIZABLE会对读取的全部行都进行加锁。

 

乐观锁和悲观锁:

  悲观锁:指悲观的认为,须要访问的数据随时可能被其余人访问或者修改。所以在访问数据以前,对要访问的数据加锁,不容许其余其余人对数据进行访问或者修改。上述讲到的服务器锁和InnoDB锁都属于悲观锁。

  乐观锁:指乐观的认为要访问的数据不会被人修改。所以不对数据进行加锁,若是操做的时候发现已经失败了,则从新获取数据进行更新(如CAS),或者直接返回操做失败。

  电商卖东西的时候,必须解决的是超卖的问题,超卖是指商品的数量好比只有5件,结果卖出去6件的状况。咱们用代码来演示一下怎么用乐观锁和悲观锁解决这个问题。假设test_prodcut表中,S001和S002的产品各有100件。

@Service
public class ProductService implements IProductService {

    @Resource
    private ProductMapper productMapper;

    private static final String product_code = "S001";

    private static final String product_code1 = "S002";

    //乐观锁下单成功数
    private final AtomicInteger optimisticSuccess = new AtomicInteger(0);

    //乐观锁下单失败数
    private final AtomicInteger optimisticFalse = new AtomicInteger(0);

    //悲观锁下单成功数
    private final AtomicInteger pessimisticSuccess = new AtomicInteger(0);

    //悲观锁下单失败数
    private final AtomicInteger pessimisticFalse = new AtomicInteger(0);

    
    //乐观锁下单
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void orderProductOptimistic() throws TestException {
        int num = productMapper.queryProductNumByCode(product_code);
        if (num <= 0) {
            optimisticFalse.incrementAndGet();
            return;
        }
        int result = productMapper.updateOrderQuantityOptimistic(product_code);
        if (result == 0) {
            optimisticFalse.incrementAndGet();
            throw new TestException("商品已经卖完");
        }
        optimisticSuccess.incrementAndGet();
    }

    //获取售卖记录
    @Override
    public String getStatistics() {
        return "optimisticSuccess:" + optimisticSuccess + ", optimisticFalse:" + optimisticFalse + ",pessimisticSuccess:" + pessimisticSuccess + ", pessimisticFalse:" + pessimisticFalse;
    }

    //悲观锁下单
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void orderProductPessimistic() {
        int num = productMapper.queryProductNumByCodeForUpdate(product_code1);
        if (num <= 0) {
            pessimisticFalse.incrementAndGet();
            return;
        }
        productMapper.updateOrderQuantityPessimistic(product_code1);
        pessimisticSuccess.incrementAndGet();
    }

    //获取产品详情
    @Override
    @Transactional
    public ProductResutl getProductDetail() {
        Random random = new Random();
        String code = random.nextInt() % 2 == 0 ? product_code : product_code1;
        ProductResutl productResutl = productMapper.selectProductDetail(code);
        return productResutl;
    }

    //清楚记录   
    @Override
    public void clearStatistics() {
        optimisticSuccess.set(0);
        optimisticFalse.set(0);
        pessimisticSuccess.set(0);
        pessimisticFalse.set(0);
    }
}

 

  对应sql以下。

 1     <update id="updateOrderQuantityPessimistic">
 2         update test_product set quantity=quantity-1 where code=#{productCode}
 3     </update>
 4 
 5     <update id="updateOrderQuantityOptimistic">
 6         update test_product set quantity=quantity-1 where code=#{productCode} and  quantity>0;
 7     </update>
 8 
 9     <select id="queryProductNumByCode" resultType="java.lang.Integer">
10         SELECT quantity From test_product WHERE code=#{productCode}
11     </select>
12 
13 
14     <select id="queryProductNumByCodeForUpdate" resultType="java.lang.Integer">
15         SELECT quantity From test_product WHERE code=#{productCode} for update
16     </select>
17 
18     <select id="selectProductDetail" resultType="com.chinaredstar.jc.crawler.biz.result.product.ProductResutl">
19         SELECT
20               id as id,
21               code as code,
22               name as name,
23               price as price,
24               quantity as quantity
25         FROM test_product WHERE code=#{productCode}
26     </select>

  测试工具使用JMeter,开启200个线程,分别对经过乐观锁和悲观锁进行下单。

  悲观锁下单结果:

  乐观锁下单结果:

  售卖状况以下:

  结果显示乐观锁和悲观锁都能成功的防止产品超卖,上述的数据比较粗糙,不能表明实际生产中的一些状况,可是在不少时候。使用乐观锁由于不须要对数据加锁,防止锁冲突,可能获得更好的性能。可是也不表明乐观锁比悲观锁更好,仍是看具体的生产状况,来判断须要的是乐观锁仍是悲观锁。

相关文章
相关标签/搜索