用作学习,如有错误,欢迎指正
根据分类方式的不同,有多重理解方式
乐观锁: 乐观锁,见名知意,其设计理念是乐观的,默认不会发生线程并发安全问题,所以在代码中是不会添加锁的,但是在进行数据库写入时会进行一个判断,判断将要修改的数据是否被别的线程修改,通常是通过数据库表中某个字段的比对来做的判断,一般使用version字段专用于乐观锁判断使用
悲观锁: 悲观锁,理念为悲观的,默认认为在代码执行中是肯定会发生线程并发安全问题,所以默认是加锁的,如Synchronization ,ReentrantLock都是悲观锁..
这是根据未获取到锁的处理方式分类的.
阻塞锁: 未获取到锁的线程会被阻塞,线程进入阻塞状态(block),不消耗CPU性能,等待锁释放时会唤醒线程重新参与竞锁.
自旋锁: 未获取到锁的线程会进入自旋状态,不断地重复获取锁的操作直至获取到,这个过程中代码是一直在运行的所以会一直的使用CPU,称为忙等.
公平锁: 在竞锁时按先后顺序进行排队获取锁
非公平锁: 在竞锁时不按照先后顺序获取锁
重入锁:支持同一个线程重复获取同一把锁
不可重入锁:同一线程不可以重复获取同一把锁
lock 锁需要自己去进行加解锁操作,解锁通常在 try catch 结构中的 finally 中,因为要保证枷锁以后必定解锁,避免死锁
syncronized 关键字,可以加在代码块上或者加在方法上
syncronized 底层是基于 jvm 的互斥锁实现的,在旧版本中,syn 关键字不存在锁升级原理,只有重量级锁,加解锁的话就涉及 jvm 内核态和用户态的转换,这就会比较效率低。所以后来引入了锁的升级原理
a.偏向锁
在多线程中,其实大多数时候都是同一个线程去重复不断的获取同一把锁,如果正常加解锁的话,效率低下,每次获取锁都会加锁与解锁,所以偏向锁就是针对这种情况的一个方式,在线程不存在竟锁的情况时,锁会将线程 id 绑定至锁头的标志位,在下次同一线程进来的时候就不会在执行加锁的操作极大的提高了效率
b.轻量级锁
在发生竞锁时,偏向锁会升级成为轻量级锁,未能获取到锁的线程将会自旋
c.重量级锁
在自旋超过 15 次以后轻量级锁会升级成为重量级锁,重量级锁中,未获取到锁的线程将会阻塞
悲观锁: 与上方概念相同,在mysql中,通常在select后面加上for update使用,他是根据数据库的锁来实现的
乐观锁: 默认不加锁,在mysql中通常是使用版本号来实现,version.进行修改之前先进行版本比较,符合版本的我才能修改.
共享锁: 又称读锁,通常在读取数据的时候使用,指多个事务可以同时获取同一个锁,在一个线程加上读锁以后,其他事务就只能加读锁,只要有一个线程加上其他锁,共享锁就失效了.使用方法,在查询语句后面加上lock in share mode
排他锁: 又称写锁,一个事务加上写锁以后,其他事务就不能加锁了,必须等待锁释放以后才能加锁.使用方法,在语句后面加上for update.
间隙锁: 见名知意,锁住一个间隙,在对这个间隙进行操作的时候,无法进行 数据修改
比如: 我有一组数据,id自增,分别为1,2,4,5,6.现在我用间隙锁锁住这些数据进行操作,现在第二个操作想添加一个id为3的数据,但是由于这里是被间隙锁锁住,所以不能添加
临键锁:与间隙锁一样,只是将含头不含尾,再次用上面那个例子,间隙锁锁住的只有3这个位置,而临键锁锁住的是2到4这个区间.
事务可以看做一组操作,这些操作要么同时成功,要么同时失败,但凡有一个操作不成功则全部失败
原子性: 一个事务里所有的操作,要么同时成功要么同时失败
持久性: 在事务进行提交以后,数据就不会丢失,因为已经被持久化到磁盘
隔离性: 多个事务是相互隔离的,谁也不影响谁
一致性: 在进行持久化的时候要满足预先的规则,也就是数据不能出错
通过Mvcc(Multi-Version Concurrency Control 多版本并发控制)在不同时期保存不同版本的版本快照到undo-log(回滚日志)中,在事务失败以后执行回滚
删除数据时回滚,读取undo-log获取删除前的数据,再写入数据库
添加数据时回滚,将添加的数据删除
修改数据时,以undolog中的数据进行覆盖.
通过innoDb中的redo-log, Mysql事务提交以后数据不是第一时间直接持久化到磁盘的,他是保存到redo-log然后持久化redo-log后就直接进行事务提交,在提交以后redo-log去进行刷盘,才持久化到磁盘.
好处是:
redo-log进行刷盘比对数据页刷盘效率高,具体表现如下
redo log体积小,毕竟只记录了哪一页修改了啥,因此体积小,
刷盘快。
redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机
IO来的快
Mysql在事务并发的时候有以下这些问题会发生
脏读: 读取到其他事务未提交的数据
不可重复读: 在重复读取同一个数据时,按道理是应该读取到一样的数据,但是在有其他事务进行提交以后其中有几次读取到了不同的数据这就是不可重复读
幻读: 与不可重复读类似,幻读是重复读取一批数据时,有其他事务进行了数据的增删,导致我这一个事务读取到的数据条数不一致
一类更新丢失(回滚更新丢失):两个事务同时对同一条数据进行修改操作,第一个事务提交数据之后,第二个执行回滚操作,这样回滚的数据就更新到实际数据上,第一次的事务就相当于没有
二类更新丢失(覆盖更新丢失): 两个事务同时对同一条数据进行修改操作,第一个事务提交事务以后,第二个事务在进行提交,第二次事务提交将第一次事务提交覆盖掉,第一次事务提交就丢失了
Mysql的事务隔离级别:
1.读未提交: 可读取到其他事务未提交的数据
2.读已提交: 只能读取到其他事物已提交的数据,解决了脏读问题
3.可重复读: 解决了不可重复读的问题,在mysql中,通过间隙锁也在这个级别解决了幻读的问题,这是Mysql的默认隔离级别.
4.串行化: 相当于将所有事务放入队列中,一个一个执行,解决以上所有问题,因为根本不存在事务并发了.但是效率低下
基本逻辑就是在redis 中保存一个 key,这个 key 是不可重复的,通过指令 setNx 实现。
保存失败的线程就是没有获取到锁
但是这里会有几个问题
在我加锁以后,我系统宕机了,锁一直没办法删除,其他线程获取不到锁,这时候需要给 key 加一个过期时间解决这个问题,但是还有问题,如果我在 setNx 和设置过期时间之间宕机,一样会出现死锁问题,这就是代码原子性的问题,所以可以使用 set 指令来设置保存的方式以及过期时间,这样两个操作就在一句代码中实现了。
有的时候,我们的算法处理时间很长,无法预估,这时候我们设置锁的过期时间的话很容易出现一个情况,上一个线程还没执行完,第二个线程已经获取到锁,然后开始执行了,然后第一个线程执行完毕以后要删除锁,但是现在的锁是第二个线程的锁,这就是锁的误删除。如何解决呢?在创建锁的时候,将线程 id 存入锁的 value 中,然后在删除锁(包括过期的时候)的时候进行一次校验,将 key 的 value 与线程 id 进行比较一样才能删除。这里也有代码原子性问题,可以用 lua 脚本处理。
redisson 的逻辑和上面讲的差不多,但是他有一个看门狗机制来解决锁的误删除问题。看门狗可以理解为一个定时任务,它默认是每三十秒检查一次线程是否执行完毕,没有的话就对锁执行一次续期。注意,看门狗如果是手动设置时间以后,就不再拥有续期的功能。
zookeeper 实现分布式锁与 Redis 逻辑上一样,就是是否存进去,但是 zookeeper 有一点区别,Zookeeper是通过临时节点来实现分布式锁的,
根据临时节点的特性实现分布式锁,先进入的线程在zookeeper中创建临时节点,后面的线程需等待上一个节点删除,节点删除说明锁释放,其他线程才能获取到锁
使用临时顺序节点实现分布式锁,参与竞锁的所有线程按顺序在zookeeper中创建临时顺序节点,在首位的节点才算获取到锁,其他的监听首位节点是否被删除,监听到首位节点被删除(锁被释放后),第二位转为第一位,从而获取到锁.
原理:
利用了Mysql的Id唯一的特性实现,手动设置id,在获取锁的时候保存一个数据进库中,其他的线程就无法保存数据,但是这种方式效率很低,尽管有数据库连接池,也很低,所以大多时候我们会去选择使用Redis或者zookeeper来实现分布式锁.
zookeeper具有强一致性,虽然牺牲了一定的性能,但能保证高可用,所以对追求可靠性较高的场景,可使用zookeeper实现分布式锁;
redis具有最终一致性,特别是在redis主从架构时,只要master上锁的元数据更新了,就立即返回给客户端ok,后边会慢慢同步给slave,牺牲了可靠,对于追求性能的场景,可使用redis实现分布式锁;
另一方面我会根据项目已有的技术栈进行选择,比如项目本来就是使用redis而没有使用zk,那么我会优先考虑Redis