死锁

image.png
用个通俗的例子讲一讲死锁(红蓝钥匙)
死锁的出现必须满足以下四个必要条件:

  1. 互斥条件 —— 只有一副钥匙
  2. 请求和保持条件 —— 拿着红钥匙的人在没有归还红钥匙的情况下,又提出要蓝钥匙
  3. 不剥夺条件 —— 人除非归还了钥匙,不然一直占用着钥匙
  4. 环路等待条件 —— 拿着红钥匙的人在等蓝钥匙,同时那个拿着蓝钥匙的人在等红钥匙

    乐观锁和悲观锁

锁机制 - 图2

InnoDB的加锁方法

表锁

意向锁是 InnoDB 自动加的,不需要用户干预。当事务要在记录上加上行锁时,要首先在表上加上意向锁。这样判断表中是否有记录正在加锁就很简单了,只要看下表上是否有意向锁就行了,从而就能提高效率。

行锁

  • 行锁是作用在索引上的,当查询列(where条件)没有设置索引时,InnoDB就会进行表锁。
  • 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及的数据集加上排他锁
  • 对于普通的SELECT语句,InnoDB不会加任何锁,使用显式加锁:
    • select for update

确保自己查找到的数据一定是最新数据,并且查找到后的数据值允许自己来修改

  • select lock in share mode

确保自己查询到的数据是最新的数据,有可能其他事务也对同数据集使用了 in share mode 的方式加上了S锁。

sql语句层面(原子操作)和事务层面的锁(个人理解)

业务中并发数据一致性问题基本都是多个事务间的竞争,场景通常是多个线程都先读到一个数据,然后经过一系列操作再写数据,因此所谓的悲观锁、乐观锁都是在事务层面加锁。如果每个事务就只是一条SQL语句进行写操作(例如update items set quantity = quantity - 1 where id = 1 and quantity - 1 > 0;),那就直接利用mysql数据库引擎隐式的加锁机。

  • sql语句层面的锁:本身就是一个原子操作
    • 隐式(update/insert/delete)的读锁、写锁、意向锁
  • 事务层间的锁:事务本身不是原子操作,通过某种方式成为原子操作

    • 显示的使用select for update
    • CAS相当于把事务的读、写组装成一个原子操作
    • Java Synchronized

      例子

      悲观: MySql for update

  • 必须关闭 MySQL 数据库的自动提交属性set autocommit=0。因为MySQL 默认使用 autocommit 模式

  • 行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。
    1. begin;
    2. SELECT quantity from items where id = 1 for update;
    3. update items set quantity = 2 where id = 1;
    4. commit;
    1. // 参考:https://www.jianshu.com/p/f5ff017db62a
    2. /**
    3. * 更新库存(使用悲观锁)
    4. * @param productId
    5. * @return
    6. */
    7. public boolean updateStock(Long productId){
    8. //先锁定商品库存记录
    9. ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId);
    10. if (product.getNumber() > 0) {
    11. int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
    12. if(updateCnt > 0){ //更新库存成功
    13. return true;
    14. }
    15. }
    16. return false;
    17. }

    乐观:CAS的方式,利用现有递增/减字段

    1. SELECT quantity as q from items where id = 1;
    2. update items set quantity = 2 where id = 1 and quantity = q;
    3. -- 或者: update items set quantity = q - 1 where id = 1 and quantity = q;
    1. // 参考:https://www.jianshu.com/p/f5ff017db62a
    2. /**
    3. * 下单减库存
    4. * @param productId
    5. * @return
    6. */
    7. public boolean updateStock(Long productId){
    8. int updateCnt = 0;
    9. while (updateCnt == 0) {
    10. ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
    11. if (product.getNumber() > 0) {
    12. updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
    13. if(updateCnt > 0){ //更新库存成功
    14. return true;
    15. }
    16. } else { //卖完啦
    17. return false;
    18. }
    19. }
    20. return false;
    21. }

    乐观:CAS方式,版本控制,解决ABA

    1. SELECT site as s, version as v from records where id = 1;
    2. update records set site = s + 1 where id = 1 and version = 1;

    乐观:解决CAS循环等待

    不算CAS思想,只是利用了一个update语句是个原子操作的范围安全检测的乐观锁
    1. -- 修改商品库存
    2. update items set quantity = quantity - 1 where id = 1 and quantity - 1 > 0;

    CAS操作

    atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现:

    • V 内存地址存放的实际值
    • O 预期的值(旧值)
    • N 更新的新值。

    当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。 当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程

CAS的ABA问题

小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50 线程1(提款机):获取当前值100,期望更新为50, 线程2(提款机):获取当前值100,期望更新为50, 线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50 线程3(默认):获取当前值50,期望更新为100, 这时候线程3成功执行,余额变为100, 线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!! 此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。

CAS的自旋

使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。

并发包Atomic例子

Atomic 原子类。对于 count++ 的操作,Java 并发包下面提供了一系列的 Atomic 原子类,比如说 AtomicInteger

  1. //import java.util.concurrent.atomic.AtomicInteger;
  2. public static void main(String[] args) {
  3. public static AtomicInteger count = new AtomicInteger(0);
  4. public static void increase() {
  5. count.incrementAndGet();
  6. }
  7. }

Actor