一致性非锁定读(consistent nonlocking read)
一致性非锁定读是指InnoDB存储引擎通过多版本控制(multi versionning)的方式来读取当前执行时间数据库中的行数据。
如果读取的行正在执行update或delete操作,这时读取操作不会因此等待行上的锁释放。相反的,InnonDB会去读取行上的一个快照数据。
快照数据是指该行之前版本的数据,该实现是通过undo page来完成。而undo用来事务中的回滚数据,因此快照数据没有额外的开销。而读取快照是不需要加锁的,没有事务会操作历史数据。
快照其实是当前行数据之前的历史版本,每行记录可能有多个版本的快照数据,一般称这种技术叫多版本并发控制(MVCC,Multi Version Concurrency Control)
非锁定读提高了数据库的并发性,在InnonDB存储引擎下,这是默认的读写方式,即读不会占用和等待表上的锁。
但是在不同的事务隔离级别下,读取的方式还有些不同。并不是都采用非锁定的一致性读,此外对快照的定义也不相同。
即快照有两个级别:
语句级
针对于Read committed隔离级别
事务级别
针对于Repeatable read隔离级别
一致性锁定读(consistent locking read)
有些情况下为了保证数据的一致性,需要对select的操作加锁。InnonDB存储引擎对于select语句支持两种一致性的锁定读。
- select …… for update
- select …… lock in share mode
select …… for update是对读取的记录加一个X锁,select …… lock in share mode是对读取的记录加一个S锁。
即使被读取的行被加了一致性锁定读,如果有另一个一致性非锁定读的操作来读取该行数据是不会阻塞的,读取的是改行的快照版本。
SELECT …… FOR UPDATE和SELECT …… LOCK IN SHARE MODE必须在一个事务中,当一个事务提交了,锁就释放了。因此在使用上述两个SELECT锁定语句时,必须开启事务。
针对RR隔离级别,在第一次创建readview后,这个readview就会一直持续到事务结束,也就是说在事务执行过程中,数据的可见性不会变,所以在事务内部不会出现不一致的情况。
针对RC隔离级别,事务中的每个查询语句都单独构建一个readview,所以如果两个查询之间有事务提交了,两个查询读出来的结果就不一样。
从这里可以看出,在InnoDB中,RR隔离级别的效率是比RC隔离级别的高。
此外,针对RU隔离级别,由于不会去检查可见性,所以在一条SQL中也会读到不一致的数据。
针对串行化隔离级别,InnoDB是通过锁机制来实现的,而不是通过多版本控制的机制,所以性能很差。
关于一致性非锁定读的一个并发问题:
商品加购,库存减少。
前提:
- 商品根据sku添加分布式redis锁,保证一次只有一个客户端持有锁
- 代码块级Synchronized为多线程加锁
修改前代码实现:
Java伪代码
if(product.stock > 0){
product.dedstock(skuid);
}
扣减库存sql
update inventory set stock = stock - 1 where skuid = skuid;
bug:
每次最终库存仍是-1
原因分析:
MySQL默认事务隔离级别为RR,读取的快照为事务开始时候的快照
事务1:stock=1
事务2:stock=1
事务1:扣减库存,stock=0
事务2:扣减库存,stock=-1
修改后代码实现:
if(product.stock > 0){
num = product.stock;
num = num -1;
product.dedstock(skuid,num);
}
扣减库存sql:
update inventory set stock = num where skuid = skuid;