Java与Mysql锁相关知识总结

科技资讯 投稿 11600 0 评论

Java与Mysql锁相关知识总结

锁的定义

锁的实现

锁在计算机底层的实现,依赖于CPU提供的CAS指令(compare and swsp),对于一个内存地址,会比较原值以及尝试去修改的值,通过值是否修改成功,来表示是否强占到了这个锁。

JVM中的锁

synchronized

synchronized是java提供的关键字锁,可以锁对象,类,方法。
在JDK1.6以后,对synchronized进行了优化,增加了偏向锁和轻量锁模式,现在synchronized锁的运行逻辑如下:

    在初始加锁时,会增加偏向锁,即“偏向上一次获取该锁的线程”,在偏向锁下,会直接CAS获取该锁。该模式大大提高了单线程反复获取同一个锁的吞吐情况,在Java官方看来,大部分锁的争抢都发生在同个线程上。
  1. 如果偏向锁CAS获取失败,说明当前线程与偏向锁偏向的线程不同,偏向锁就会升级成轻量锁,轻量锁的特点就是通过自旋CAS去获取锁。
  2. 如果自旋获取失败,那么锁就会升级成重量锁,所有等待锁的线程将被JVM挂起,在锁释放后,再由JVM统一通知唤醒,再去尝试CAS锁,如果失败,继续挂起。

轻量锁设计的目的是“在短期内,锁的争抢通过自旋CAS就可以获取到,短时间内的CPU自旋消耗小于线程挂起再唤醒的消耗”。
重量锁就是最初优化前的synchronized的逻辑了。

ReentrantLock

AQS全称AbstractQueueSynchronizer,几乎JUC里所有的工具类,都依赖AQS实现。
AQS在java里,是一个抽象类,但是本质上是一种思路在java中的实现而已。
AQS的实现逻辑如下:

    构造一个队列
  1. 队列中维护需要等待锁的线程
  2. 头结点永远是持有锁(或持有资源)的节点,等待的节点在头结点之后依次连接。
  3. 头结点释放锁后,会按照顺序去唤醒那些等待的节点,然后那些节点会再次去尝试获取锁。

    是在java api层面实现的锁,所以可以实现各种并发工具类,操作也更加灵活
  1. 因为提供了超时时间等机制,操作灵活,所以不易死锁。(相同的,如果发生死锁,将更难排查,因为jstack里将不会有deadlock标识)。
  2. 可以实现公平锁,而synchronized必定是非公平锁。
  3. 因为是JavaApi层实现的锁,所以可以响应中断。

到这里你会发现,其实ReentrantLock可以说是synchronized在JavaApi层的实现。

Mysql 锁

共享锁(S 与排它锁(X

作用范围

获取共享锁时,如果该数据被其他事务的排它锁锁住,则无法获取,需要等待排它锁释放。

意向锁

作用范围

意向锁为表锁,在获取表锁之前,一定会检查意向锁。

在事务获得表中某行的共享锁之前,它必须首先获得表上的 IS 锁或更强的锁。

在获取任意表锁的共享锁或排它锁之前,一定会检查该表上的共享锁。

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

意向锁的作用在于:在获取表锁时,可以通过意向锁来快速判断能否获取。

特别注意的是,意向锁是可以叠加的,即会存在多个,如T1事务获取了意向锁IX1和行级锁X1,T2事务依旧可以获取意向锁IX2和行级锁X2,所以仅在获取表级锁之前,才会检查意向锁。

记录锁

记录锁在没有索引时依旧会生效,因为innodb会为每张表创建一个隐藏的索引。

间隙锁

间隙锁生效在索引上,用于锁定索引值后的行,防止插入,在select from table where index=? for update时会生效,例如index=1,则会锁住index=1索引节点相关的行,防止其他事务插入数据

但是并不会防止update语句,哪怕update的数据不存在。

Next-Key Locks

Insert Intention Locks

插入意向锁,和意向锁类似。不过是特殊的间隙锁,并不发生在select for update,而是在同时发生insert时产生,例如在两个事务同时insert索引区间为[4,7]时,同时获得该区间的意向锁,此时事务不会阻塞,例如A:insert-5,B:insert-7,此时不会阻塞两个事务。

AUTO-INC Locks

自增锁,这个锁很明显是表级insert锁,为了保证自增主键的表的主键保持原子自增。

常见锁使用的场景和用法

double check

众所周知,mysql的事务对防止重复插入并没有什么卵用,唯一索引又存在很多缺点,业务上最好不要使用,所以一般来说防止重复插入的通用做法就是使用分布式锁,这就有一种比较常用的写法。

final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId(;
if (weekendNoticeReadCountDO == null {
    final String lockKey = RedisConstant.LOCK_WEEKEND_READ_COUNT_INSERT + ":" + noticeRequestDTO.getNoticeId(;
    ClusterLock lock = clusterLockFactory.getClusterLockRedis(
        RedisConstant.REDIS_KEY_PREFIX,
        lockKey
    ;
    if (lock.acquire(RedisConstant.REDIS_LOCK_DEFAULT_TIMEOUT {
        //double check
        final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId(;
        if (weekendNoticeReadCountDO == null {
            try {
                lock.execute(( -> {
                    WeekendNoticeReadCountDO readCountDO = new WeekendNoticeReadCountDO(;
                    readCountDO.setNoticeId(noticeRequestDTO.getNoticeId(;
                    readCountDO.setReadCount(1L;
                    readCountDO.setCreateTime(new Date(;
                    readCountDO.setUpdateTime(new Date(;
                    weekendNoticeReadRepositoryService.insert(readCountDO;
                    return true;
                };
            } catch (ApiException err {
                throw err;
            } catch (Exception e {
                log.error("插入", e;
                throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(, "服务端出错";
            }
        } else {
            weekendNoticeReadRepositoryService.noticeCountAdd(weekendNoticeReadCountDO;
        }
    } else {
        log.warn("redis锁获取超时,key:{}", lockKey;
        throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(, "服务器繁忙,请稍后重试";
    }
} 

在获取到锁之后,可能是经过等待才获取到的锁,此时上一个释放锁的线程可能已经插入了数据了,所以在锁内部,依旧要再次校验一下数据是否存在。
这种写法适合大多数需要唯一性的写场景。

避免死锁

也要注意一些隐性锁,比如数据库。
事务A:

    插入[5,7],插入意向锁。
  1. select for update更新[100,150],间隙锁。
    事务B:
  2. select for update更新[90,120],间隙锁。
  3. 插入[4,6],插入意向锁。

**

顺带谈谈并发场景下常见的问题

读写混乱

比如构建一个static缓存,没有使用ConcurrentHashMap中的putIfAbsent等方法,也没有加锁去构建,导致上面的线程刚put了,下面的线程就删掉了,或者重复构建2次缓存。

Redis或者一些并发操作释放锁或者资源,没有检查是否是当前线程持有

线程A获取到锁,此时B,C在等待,然后A执行时间过长,导致锁超时被自动释放了,此时B获取到了锁,在快乐的执行,然后A执行完了之后,释放锁时没有判断是否还是自己持有,导致B持有的锁被删除了,此时C又获取到了锁,BC同时在执行。

编程笔记 » Java与Mysql锁相关知识总结

赞同 (57) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽