innodb锁机制探究(二)---间隙锁(2) 上一篇文章中,我们已经知道innodb中的间隙锁是对普通索引记录的间隙做的一个锁定动作,这篇文章我们分析下间隙锁在唯一索引中的应用。 也就是说,不存在gap锁。 再看下一个实验: ? 我们可以看到,在我们搜索age=15的时候,这条记录是不存在的,那么在session B中插入id=14的这条记录的时候,我们发现是无法插入的,产生了锁等待,这就说明当记录不存在的时候,唯一索引中也会产生间隙锁 总结一下 当字段是唯一索引或者主键的时候,间隙锁产生的规则如下: 1、如果查询的结果中包含这个记录,那么在另外的会话上插入该记录前后间隙的记录时,不会产生间隙锁 2、如果查询的结果中不包含这个记录,那么在另外的会话上插入该记录前后间隙的记录时 ,会产生间隙锁。
// Innodb锁机制探究(一)---自增锁(2) // 之前我们说过一篇关于MySQL的自增锁,但是没有系统的做测试,今天做一点测试,看看效果。 通过上面这张图我们可以看到,当我们在一个事务中进行自增列的insert操作时候,另外一个会话中又进行了插入记录的操作,在这种情况下,会发生2个奇怪的现象: 1、会话1中的自增列好像直接增加了2个值。 2、会话2中的自增列直接从2开始增加。 那么为什么表级别的锁,我们还能够在会话1中的事务没有结束的时候,在另外一个会话2上成功执行insert呢?不应该直接锁表么? 2、对于常规的insert操作,可以使用参数innodb_autoinc_lock_mode来控制是否使用表级别的锁,如果该参数是0,则使用表级别的auto_inc 锁,如果该参数是1,则使用互斥自增长机制实现主键的自增
> 例如:一个锁系统可以同时包含以下与单个资源(表report的row#2)有关的锁 <transaction#3305, row#2 of table `report`, shared, granted > <transaction#3305, row#2 of table `report`, exclusive, granted > <transaction#3306, row#2 of table | report | S | GRANTED | | 3305 | 2 | report | X | GRANTED | | 3306 | : con1> LOCK TABLES t READ; Query OK, 0 rows affected (0.00 sec) 您可能希望事务已锁定表t,但是看不到任何锁: con2> SELECT 因此,我只是说这个表显示了服务器获取的锁,阻止了其他客户端尝试修改表: con3> insert into test.t values (10); ⌛ 将等待,您可以通过以下方式进行验证: con2>
开启一个队列 让命令进入队列 执行事务 # 1 开启事务 multi # 2 输入命令 set k1 v1 set k2 v2 get k2 set k2 v3 get k2 # 3 执行/放弃事务 exec 或者 discard Redis 悲观锁 效率低,所有悲观锁都不建议使用 悲观锁:每次都会操作都会上锁,执行完毕就会释放锁,别人才可以获得锁。这样会导致效率低下,降低并发量。 Redis CAS乐观锁 watch操作 乐观锁,任何人操作都不上锁,但是真实操作时,如果这个key发现version变动了,本次修改的相关事务操作不会执行! 所有人都可以拿到锁,就可以提高系统吞吐量 Redis 乐观锁的使用场景是:电影院购票,比如C1这个作为有多人同时去抢,这张票只能被一个人抢成功。使用Redis乐观锁的好处是。 watch 需要锁Key名 # 线程1 操作:开启事务,并设置money为80 但不执行事务 multi set money 80 或者 decrby money 20 # 线程2 操作:读取money
java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。每个对象一开始都是无锁的,随着线程间争夺锁,越激烈,锁的级别越高,并且锁只能升级不能降级。 用2字(32位JVM中1字=32bit=4baye)存储对象头,如果是数组类型使用3字存储(还需存储数组长度)。对象头中记录了hash值、GC年龄、锁的状态、线程拥有者、类元数据的指针。 ? ? 2.撤销偏向锁 当有另一个线程来竞争锁的时候,就不能再使用偏向锁了,要膨胀为轻量级锁。 竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。 ? ? 三、轻量级锁 轻量锁与偏向锁不同的是: 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁 每次进入退出同步块都需要CAS更新对象头 争夺轻量级锁失败时,自旋尝试抢占锁 可以看到轻量锁适合在竞争情况下使用 2.解锁 用CAS操作锁置为无锁状态(偏向锁位为"0",锁标识位为"01"),若CAS操作失败则是出现了竞争,锁已膨胀为重量级锁了,此时需要释放锁(持有重量级锁线程的指针位为"0",锁标识位为"10"
# 设置超时时间可避免死锁 time.sleep(1) lock2.acquire() # lock2.acquire(timeout=2) # 设置超时时间可避免死锁 lock1.release() lock2.release() class T2(Thread): def run(self): print ("start run T2") lock2.acquire() # lock2.acquire(timeout=2) # 设置超时时间可避免死锁 time.sleep lock1.release() def test(): t1, t2 = T1(), T2() t1.start() t2.start() t1.join () t2.join() if __name__ == "__main__": test() 交替锁 如果我们要在链表中插入一个节点。
index2获取了lock index2线程获取到了cpu的资源,开始执行方法 uuid=v2 set(lock,uuid); index1执行删除,此时会把index2的lock删除 index1 因为已经在方法中了 index1已经比较完成了,这个时候,开始执行 删除的index2的锁! 定义一个锁:lua 脚本可以使用同一把锁,来实现删除! 也就是说锁永远存在! 重试 为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: - 互斥性。
* 2. 可重入锁框架,相比于内置锁最大的区别:范围可重叠获取锁过程允许中断和失败支持多个条件队列可以扩展出读写锁、CountDownLatch、Semphore等多种灵活的形式2. 2. 双线程,有冲突锁类型耗时(ms)相对增幅(相对第一名)synchronized448+0%1 readlock + 1 writelock1038+132%2 writelock1137+154%3. Public2cpu缓存
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。 Thread-1释放了锁 Thread-2得到了锁 Thread-3得到了锁 Thread-2释放了锁 Thread-3释放了锁 Thread-4得到了锁 Thread-4释放了锁 Thread-5得到了锁 Thread-1 :得到了锁 Thread-2获取锁失败 Thread-0获取锁失败 Thread-3获取锁失败 Thread-1 :释放了锁 Thread-4 :得到了锁 Thread-4 :释放了锁 method2。 7.4.读写锁 读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。 正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
通过锁计数器+-1,实现对锁的加锁和释放。 2 锁优化 2.1 自旋锁与自适应自旋 引入的原因是互斥同步对性能最大的影响是阻塞,挂起线程和恢复线程都需要转入内核态完成,给并发性能带来很大压力。 2.4 轻量级锁 2.5 偏向锁 大多数锁,在它们的生命周期中,从来不会被多于一个线程所访问。即使在极少数情况下,多个线程真的共享数据了,锁也不会发生竞争。 为了理解偏向锁的优势,我们首先需要回顾一下如何获取锁(监视器)。 获取锁的过程分为两部分。首先,你需要获得一份契约.一旦你获得了这份契约,就可以自由地拿到锁了。 将锁偏向于一个线程,意味着该线程不需要释放锁的契约。因此,随后获取锁的时候可以不那么昂贵。如果另一个线程在尝试获取锁,那么循环线程只需要释放契约就可以了。
T2和t2事务的is_waiting如果获取的是插入意向锁,这时候的is_Waiting就是false。 T2想对number值为3,8,15这三条记录加x型的next-key锁,加之前,先给表加ix锁,我们这里还是着重看行锁。 但是number为15的记录已经被T1加了s型正经record锁,所以T2不能获取到15的x型next-key锁,也就是生成锁的结构is_waiting为true。 因为等待状态不相同,所以这时候会生成两个锁结构。所以这时候属性如下: 事务T2要进行加锁,所以锁结构的 锁所在事务信息 指的就是T2. 对应事务信息:T2 索引信息:primary 行锁,表锁:spaceid 67,page number 3,n_bits为72。
分布式锁就是一个解决方案。 “分布式锁”是用来解决分布式应用中“并发冲突”的一种常用手段,实现方式一般有基于zookeeper及基于redis二种 自己写一个简单的 redis分布式锁 加锁时 加锁时使用 set 命令,使用 加锁执行命令 这个随机数,由客户端生成,用来标识持有锁的人,在删除时只能由持有锁的人来删除。 解锁 所以在解锁之前先判断一下是不是自己加的锁,是自己加的锁再释放,不是就不释放。 所以伪代码如下 if (random_value .equals(redisClient.get(resource_name))) { del(key) } 因为判断和解锁是2个独立的操作,不具有原子性 在尝试获取锁的时候,是非阻塞的,不满足在一定期限内不断尝试获取锁的场景。 以上两点,都可以采用 Redisson框架里的锁 解决
大纲1.Redisson可重入锁RedissonLock概述2.可重入锁源码之创建RedissonClient实例3.可重入锁源码之lua脚本加锁逻辑4.可重入锁源码之WatchDog维持加锁逻辑5.可重入锁源码之可重入加锁逻辑 其中KEYS[1]就是锁的名字如myLock,ARGV[2]为UUID + 线程ID。如果存在,说明获取锁的线程还在持有锁,并没有对锁进行释放。 getLockName(threadId)//ARGV[3] ); } ...}8.可重入锁源码之获取锁超时与锁超时自动释放逻辑(1)尝试获取锁超时(2)锁超时自动释放针对如下代码方式去获取锁 (1)加锁(2)WatchDog维持加锁(3)可重入锁(4)锁互斥(5)手动释放锁(6)宕机自动释放锁(7)尝试加锁超时(8)超时锁自动释放(1)加锁在Redis里设置两层Hash数据结构,默认的过期时间是 (2)WatchDog维持加锁如果获取锁的线程一直持有锁,那么Redis里的key就会一直保持存活。获取锁成功时会创建一个定时任务10秒后检查锁是否还被线程持有。
大纲1.Redisson可重入锁RedissonLock概述2.可重入锁源码之创建RedissonClient实例3.可重入锁源码之lua脚本加锁逻辑4.可重入锁源码之WatchDog维持加锁逻辑5.可重入锁源码之可重入加锁逻辑 里引入依赖(2)构建RedissonClient并使用Redisson(3)Redisson可重入锁RedissonLock简单使用(1)在pom.xml里引入依赖<dependencies> < 2.可重入锁源码之创建RedissonClient实例(1)初始化与Redis的连接管理器ConnectionManager(2)初始化Redis的命令执行器CommandExecutor使用Redisson.create 也就是在key为myLock的Hash值里,把field为UUID:ThreadID的value值从1累加到2,发生这种情况的时候往往就是当前线程对锁进行了重入。 那么就通过Redis的pttl命令,返回key为锁名的Hash值的剩余存活时间,因为不同线程的ARGV[2]是不一样的,ARGV[2] = UUID + 线程ID。
通常讲到并发,解决方案无非就是前端限制重复提交,后台进行悲观锁或者乐观锁限制。 ? 在java中synchronized和ReentrantLock重入锁等锁就是悲观锁,数据库中表锁、行锁、读写锁等也是悲观锁。 所以悲观锁是限制其他线程,而乐观锁是限制自己,虽然他的名字有锁,但是实际上不算上锁,通常为version版本号机制,还有CAS算法。 校验一下version版本号,发现在数据库里对应Article记录的version=2,这和我手里的版本不一样啊,说明提交的Article不是最新的,那么就不能update到数据库了,进行报错把,这样就避免了并发时数据冲突的问题 所以悲观锁和乐观锁没有绝对的好坏,必须结合具体的业务情况来决定使用哪一种方式。另外在阿里巴巴开发手册里也有提到: 如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。
' //table_locks_waited 的值越高,则说明存在严重的表级锁的争用情况 2 表锁的锁模式 是否兼容 请求none 请求读锁 请求写锁 当前处于读锁 是 是 否 当前处于写锁 是 否 否 ,必须同时取得所有涉及的表的锁,并且MySQL支持锁升级 即在执行lock tables后,只能访问显式加锁的这些表,不能访问未加锁的表 加的是读锁,就只能查询,不能更新 session1 session2 unlock tables 等待 获得锁,更新成功 在自动加锁的情况下也如此,MySQL会一次获得SQL语句所需要的全部锁 所以MyISAM的表不会死锁 session1 session2 获得表 where id=1 获得锁,更新成功 4.5.2 Innodb排他锁 session_1 session_2 set autocommit=0,select * from actor where 此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。
简介 上篇已经对锁的属性做了一个简单的介绍,此篇主要针对于不同锁的使用,分析优缺点,方便以后使用锁的时候能选择合适的锁。 methodTwo:thread-2 will leave:thread-2 从控制台输出可以看出,methodOne进入了methodTwo,说明synchronized是可重入锁; 然后thread1 (true); ReentrantReadWriteLock 此锁能获取两种类型的锁,读锁和写锁,读锁是共享锁,写锁是排他锁,读读共享,读写互斥,此锁也可以构造公平与非公平锁 我们将上面的代码改造使用ReentrantReadWriteLock public SimpleReentrantReadWriteLock(int var1,String var2) { this.var1 = var1; this.var2 read(); break; case 2: write(var2); break
当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。 当前线程执行另一个 LOCK TABLES 时, 或当与服务器的连接被关闭时,所有由当前线程锁定的表被隐含地解锁 加锁语法 LOCK TABLES t1 WRITE, t2 READ, ...; 按我经验来说基本上都是竞争太强烈导致的,比如定时器1秒执行一次,而定时器里的的代码逻辑比较复杂执行时间>1秒那么这样长久下去早晚出事 如果不上面情况那么你就需要按照下面这些情况慢慢的排查了 1)sql未使用索引,更新或删除单表中的数据 2) DDL(修改表结构…)操作或加索引操作,这也会造成锁表 重点: 第2条和第4条发生几率很高 死锁的预防措施 既然知道了锁表以后,我们有一些事后的补救措施,那我们是不是在刚开始设计的时候就可以尽可能规避这些坑呢 答案是有的,如下所示 1)对于大表的操作,查询条件一定要保证命中索引,如果能命中唯一索引就更好了 2)我们在程序开发的时候,尽可能将大事务拆分为小事务,减少锁表或回滚,比如:抽离部分业务逻辑异步发送消息队列处理
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁 共享锁:指该锁可被多个线程所持有。 对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。 使用方法 声明一个读写锁 如果需要独占锁则加从可重入读写锁里得到写锁 写锁demo 如果需要共享锁则加从可重入读写锁里得到读锁 读锁demo ReentrantReadWriteLock实现原理简单分析 清单2:读写锁状态获取 static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT ,低16位表示写锁个数 一个线程获取到了写锁,并且重入了两次,低16位是3,线程又获取了读锁,并且重入了一次,高16位就是2 读锁的写锁的获取主要调用AQS的相关Acquire方法,其释放主要用了相关Release
基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。 同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。 do { var5 = this.getIntVolatile(var1, var2); } while(! this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } 上面就是getAndAddInt 方法的实现,具体流程如下, 1、首先根据当前的传过来的对象指针,获取期望的值 var5, 2、然后while判断调用compareAndSwapInt方法 ,这是一个native本地方法,它有四个参数