MySQL 中有哪些锁?
数据库中锁的设计初衷处理并发问题,作为多用户共享资源,当出现并发访问的时候,数据库需要合理控制资源访问规则。锁就是实现这些访问规则中的重要数据。
锁的分类根据加锁范围,MySQL 里面的锁可以分成 全局锁 、 表级锁 、 行锁 三类。
全局锁全局锁,就是对整个数据库实例加锁,MySQL 提供了一个加全局读锁的方法,命令是:
Flush tables with read lock (FTWRL)当需要整个库只读状态的时候,可以使用这个命令,之后其他线程的:数据更新语句(增删改),数据定义语句(建表,修改表结构)和更新事务的提交语句将会被阻塞。
全局锁的使用场景全局锁的定型使用场景,做 全库逻辑备份 。也就是把整个库每个表都 Select 出来,然后存成文本。
如何整个库都只读,会有什么问题? 如果你在主库上备份,那么在备份期间都不能执行更想,业务就基本上停摆。 如果在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog ,会导致从延迟。 既然要全库只读, 为什么不使用set global readonly=true的方式呢readonly 方式也可以让全库进入只读状态,但我还是会建议你用FTWRL方式, 主要有两个原因:
一是, 在有些系统中, readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大, 我不建议你使用。 二是, 在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开, 那么MySQL会自动释放这个全局锁, 整个库回到可以正常更新的状态。而将整个库设置为readonly之后, 如果客户端发生异常, 则数据库就会一直保持readonly状态, 这样会导致整个库长时间处于不可写状态, 风险较高 表级别锁MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lok, MDL)。表锁的语法是 :
lock tables read/write与 FTWRL 类似,可以使用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意的是,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的 *** 作对象。
MDL 表级锁MDL 不需要显示使用,在访问一个表的时候自动加上, MDL 保证读写的正确性,也就是说在查询数据时,不允许有其他线程对这个表结构做变更。
什么 *** 作会加 MDL 锁?在MySQL 55版本中引入了MDL, 当对一个表做增删改查 *** 作的时候,加 MDL读锁 ;当要对表做结构变更 *** 作的时候,加 MDL写锁 。
读锁之间不互斥,因此可以有多个线程同时对一张表增删改查。 读写之间、写锁之间是互斥的,用来保证变更表结构 *** 作的安全性,如果有两个线程要同时给一个表加字段,其中一个要等另外一个执行完才能执行。 更改表结构要注意哪些?给一个表加字段, 或者修改字段, 或者加索引, 需要扫描全表的数据。在对大表 *** 作的时候, 你肯定会特别小心, 以免对线上服务造成影响。而实际上, 即使是小表, *** 作不慎也会出问题,导致整个库的线程爆满。
举个例子我们来看一下下面的 *** 作序列, 假设表t是一个小表。
image
session A先启动, 这时候会对表t加一个 MDL读锁 。由于session B需要的也是 MDL读锁 , 因此可以正常执行。 session C会被blocked, 是因为session A的MDL读锁还没有释放, 而session C需要MDL写锁, 因此只能被阻塞,读写锁互斥。 如果只有session C自己被阻塞还没什么关系, 但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。前面我们说了,所有对表的增删改查 *** 作都需要先申请MDL读锁, 就都被锁住, 等于这个表现在完全不可读写了。如果某个表上的查询语句频繁, 而且客户端有重试机制,也就是说超时后会再起一个新session 再请求的话, 这个 库的线程很快就会爆满 。事务中的MDL锁, 在语句执行开始时申请, 但是语句结束后并不会马上释放, 而会等到整个事务提交后再释放。
怎么解决这个 更改表结构问题比较理想的机制是, 在alter table语句里面设定等待时间, 如果在这个指定的等待时间里面能够拿到MDL写锁最好, 拿不到也不要阻塞后面的业务语句, 先放弃。
ALTER TABLE tbl_name NOWAIT add column ALTER TABLE tbl_name WAIT N add columnJava中的锁主要包括synchronized锁和JUC包中的锁,这些锁都是针对单个JVM实例上的锁,对于分布式环境如果我们需要加锁就显得无能为力。在单个JVM实例上,锁的竞争者通常是一些不同的线程,而在分布式环境中,锁的竞争者通常是一些不同的线程或者进程。如何实现在分布式环境中对一个对象进行加锁呢?答案就是分布式锁。
目前分布式锁的实现方案主要包括三种:
基于数据库实现分布式锁主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。
基于缓存实现分布式锁:理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存 *** 作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。一般使用Redis来实现分布式锁都是利用Redis的 SETNX key value 这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。
基于Zookeeper:Zookeeper一般用作配置中心,其实现分布式锁的原理和Redis类似,我们在Zookeeper中创建瞬时节点,利用节点不能重复创建的特性来保证排他性。
在实现分布式锁的时候我们需要考虑一些问题,例如:分布式锁是否可重入,分布式锁的释放时机,分布式锁服务端是否有单点问题等。
上面已经分析了基于数据库实现分布式锁的基本原理:通过唯一索引保持排他性,加锁时插入一条记录,解锁是删除这条记录。下面我们就简要实现一下基于数据库的分布式锁。
id字段是数据库的自增id,unique_mutex字段就是我们的防重id,也就是加锁的对象,此对象唯一。在这张表上我们加了一个唯一索引,保证unique_mutex唯一性。holder_id代表竞争到锁的持有者id。
如果当前sql执行成功代表加锁成功,如果抛出唯一索引异常(DuplicatedKeyException)则代表加锁失败,当前锁已经被其他竞争者获取。
解锁很简单,直接删除此条记录即可。
是否可重入 :就以上的方案来说,我们实现的分布式锁是不可重入的,即是是同一个竞争者,在获取锁后未释放锁之前再来加锁,一样会加锁失败,因此是不可重入的。解决不可重入问题也很简单:加锁时判断记录中是否存在unique_mutex的记录,如果存在且holder_id和当前竞争者id相同,则加锁成功。这样就可以解决不可重入问题。
锁释放时机 :设想如果一个竞争者获取锁时候,进程挂了,此时distributed_lock表中的这条记录就会一直存在,其他竞争者无法加锁。为了解决这个问题,每次加锁之前我们先判断已经存在的记录的创建时间和当前系统时间之间的差是否已经超过超时时间,如果已经超过则先删除这条记录,再插入新的记录。另外在解锁时,必须是锁的持有者来解锁,其他竞争者无法解锁。这点可以通过holder_id字段来判定。
数据库单点问题 :单个数据库容易产生单点问题:如果数据库挂了,我们的锁服务就挂了。对于这个问题,可以考虑实现数据库的高可用方案,例如MySQL的MHA高可用解决方案。
使用Jedis来和Redis通信。
可以看到,我们加锁就一行代码:
jedisset(String key, String value, String nxxx, String expx, int time);
这个set()方法一共五个形参:
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,这里写的是锁竞争者的id,在解锁时,我们需要判断当前解锁的竞争者id是否为锁持有者。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set *** 作;若key已经存在,则不做任何 *** 作。
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期时间的设置,具体时间由第五个参数决定;
第五个参数为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1当前没有锁(key不存在),那么久进行加锁 *** 作,并对锁设置一个有效期,同时value表示加锁的客户端。2已经有锁存在,不做任何 *** 作。
上述解锁请求中, SET_IF_NOT_EXIST (不存在则执行)保证了加锁请求的排他性,缓存超时机制保证了即使一个竞争者加锁之后挂了,也不会产生死锁问题:超时之后其他竞争者依然可以获取锁。通过设置value为竞争者的id,保证了只有锁的持有者才能来解锁,否则任何竞争者都能解锁,那岂不是乱套了。
解锁的步骤:
注意到这里解锁其实是分为2个步骤,涉及到解锁 *** 作的一个原子性 *** 作问题。这也是为什么我们解锁的时候用Lua脚本来实现,因为Lua脚本可以保证 *** 作的原子性。那么这里为什么需要保证这两个步骤的 *** 作是原子 *** 作呢?
设想:假设当前锁的持有者是竞争者1,竞争者1来解锁,成功执行第1步,判断自己就是锁持有者,这是还未执行第2步。这是锁过期了,然后竞争者2对这个key进行了加锁。加锁完成后,竞争者1又来执行第2步,此时错误产生了:竞争者1解锁了不属于自己持有的锁。可能会有人问为什么竞争者1执行完第1步之后突然停止了呢?这个问题其实很好回答,例如竞争者1所在的JVM发生了GC停顿,导致竞争者1的线程停顿。这样的情况发生的概率很低,但是请记住即使只有万分之一的概率,在线上环境中完全可能发生。因此必须保证这两个步骤的 *** 作是原子 *** 作。
是否可重入 :以上实现的锁是不可重入的,如果需要实现可重入,在 SET_IF_NOT_EXIST 之后,再判断key对应的value是否为当前竞争者id,如果是返回加锁成功,否则失败。
锁释放时机 :加锁时我们设置了key的超时,当超时后,如果还未解锁,则自动删除key达到解锁的目的。如果一个竞争者获取锁之后挂了,我们的锁服务最多也就在超时时间的这段时间之内不可用。
Redis单点问题 :如果需要保证锁服务的高可用,可以对Redis做高可用方案:Redis集群+主从切换。目前都有比较成熟的解决方案。
利用Zookeeper创建临时有序节点来实现分布式锁:
其基本思想类似于AQS中的等待队列,将请求排队处理。其流程图如下:
解决不可重入 :客户端加锁时将主机和线程信息写入锁中,下一次再来加锁时直接和序列最小的节点对比,如果相同,则加锁成功,锁重入。
锁释放时机 :由于我们创建的节点是顺序临时节点,当客户端获取锁成功之后突然session会话断开,ZK会自动删除这个临时节点。
单点问题 :ZK是集群部署的,主要一半以上的机器存活,就可以保证服务可用性。
Zookeeper第三方客户端curator中已经实现了基于Zookeeper的分布式锁。利用curator加锁和解锁的代码如下:
1、全页锁(allpages lock) 对查询的表及索引页加锁,也就是table lock
2、页锁 (data lock) 对所查询的结果所在页加锁,对索引不加锁
3、行锁 (row lock) 对某行数据加锁
好像一个lock占用的内存为120byte!
锁只是一种保护机制,并不影响数据存储!
全局锁
顾名思义,全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是Flushtableswithreadlock(FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
表级锁
MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(metadatalock,MDL)。
表锁
表锁的语法是locktablesread/write。与FTWRL类似,可以用unlocktables主动释放锁,也可以在客户端断开的时候自动释放。需要注意,locktables语法除了会限制别的线程的读写外,也限定了本线程接下来的 *** 作对象。
元数据锁
MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
当多个用户访问同一份数据时,一个用户在更改数据的过程中,可能有其他用户同时发起更改请求,为保证数据库记录的更新从一个一致性状态变为另外一个一致性状态,使用事务处理是非常必要的,事务具有以下四个特性:
MySQL 提供了多种事务型存储引擎,如 InnoDB 和 BDB 等,而 MyISAM 不支持事务。为了支持事务,InnoDB 存储引擎引入了与事务处理相关的 REDO 日志和 UNDO 日志,同时事务依赖于 MySQL 提供的锁机制
事务执行时需要将执行的事务日志写入日志文件,对应的文件为 REDO 日志。当每条 SQL 进行数据更新 *** 作时,首先将 REDO 日志写进日志缓冲区。当客户端执行 COMMIT 命令提交时,日志缓冲区的内容将被刷新到磁盘,日志缓冲区的刷新方式或者时间间隔可以通过参数 innodb_flush_log_at_trx_commit 控制
REDO 日志对应磁盘上的 ib_logifleN 文件,该文件默认为 5MB,建议设置为 512MB,以便容纳较大的事务。MySQL 崩溃恢复时会重新执行 REDO 日志的记录,恢复最新数据,保证已提交事务的持久性
与 REDO 日志相反,UNDO 日志主要用于事务异常时的数据回滚,具体内容就是记录数据被修改前的信息到 UNDO 缓冲区,然后在合适的时间将内容刷新到磁盘
假如由于系统错误或者 rollback *** 作而导致事务回滚,可以根据 undo 日志回滚到没修改前的状态,保证未提交事务的原子性
与 REDO 日志不同的是,磁盘上不存在单独的 UNDO 日志文件,所有的 UNDO 日志均存在表空间对应的 ibd 数据文件中,即使 MySQL 服务启动了独立表空间
在 MySQL 中,可以使用 BEGIN 开始事务,使用 COMMIT 结束事务,中间可以使用 ROLLBACK 回滚事务。MySQL 通过 SET AUTOCOMMIT、START TRANSACTION、COMMIT 和 ROLLBACK 等语句支持本地事务
MySQL 定义了四种隔离级别,指定事务中哪些数据改变其他事务可见、哪些数据该表其他事务不可见。低级别的隔离级别可以支持更高的并发处理,同时占用的系统资源更少
InnoDB 系统级事务隔离级别可以使用以下语句设置:
查看系统级事务隔离级别:
InnoDB 会话级事务隔离级别可以使用以下语句设置:
查看会话级事务隔离级别:
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。读取未提交的数据称为脏读(Dirty Read),即是:首先开启 A 和 B 两个事务,在 B 事务更新但未提交之前,A 事务读取到了更新后的数据,但由于 B 事务回滚,导致 A 事务出现了脏读现象
所有事务只能看见已经提交事务所做的改变,此级别可以解决脏读,但也会导致不可重复读(Nonrepeatable Read):首先开启 A 和 B 两个事务,A事务读取了 B 事务的数据,在 B 事务更新并提交后,A 事务又读取到了更新后的数据,此时就出现了同一 A 事务中的查询出现了不同的查询结果
MySQL 默认的事务隔离级别,能确保同一事务的多个实例在并发读取数据时看到同样的数据行,理论上会导致一个问题,幻读(Phontom Read)。例如,第一个事务对一个表中的数据做了修改,这种修改会涉及表中的全部数据行,同时第二个事务也修改这个表中的数据,这次的修改是向表中插入一行新数据,此时就会发生 *** 作第一个事务的用户发现表中还有没有修改的数据行
InnoDB 通过多版本并发控制机制(MVCC)解决了该问题:InnoDB 通过为每个数据行增加两个隐含值的方式来实现,这两个隐含值记录了行的创建时间、过期时间以及每一行存储时间发生时的系统版本号,每个查询根据事务的版本号来查询结果
通过强制事务排序,使其不可能相互冲突,从而解决幻读问题。简而言之,就是在每个读的数据行上加上共享锁实现,这个级别会导致大量的超时现象和锁竞争,一般不推荐使用
为了解决数据库并发控制问题,如走到同一时刻客户端对同一张表做更新或者查询 *** 作,需要对并发 *** 作进行控制,因此产生了锁
共享锁的粒度是行或者元组(多个行),一个事务获取了共享锁以后,可以对锁定范围内的数据执行读 *** 作
排他锁的粒度与共享锁相同,一个事务获取排他锁以后,可以对锁定范围内的数据执行写 *** 作
有两个事务 A 和 B,如果事务 A 获取了一个元组的共享锁,事务 B 还可以立即获取这个元组的共享锁,但不能获取这个元组的排他锁,必须等到事务 A 释放共享锁之后。如果事务 A 获取了一个元组的排他锁,事务 B 不能立即获取这个元组的共享锁,也不能立即获取这个元组的排他锁,必须等到 A 释放排他锁之后
意向锁是一种表锁,锁定的粒度是整张表,分为意向共享锁和意向排他锁。意向共享锁表示一个事务有意对数据上共享锁或者排他锁。有意表示事务想执行 *** 作但还没真正执行
锁的粒度主要分为表锁和行锁
表锁的开销最小,同时允许的并发量也是最小。MyISAM 存储引擎使用该锁机制。当要写入数据时,整个表记录被锁,此时其他读/写动作一律等待。一些特定的动作,如 ALTER TABLE 执行时使用的也是表锁
行锁可以支持最大的并发,InnoDB 存储引擎使用该锁机制。如果要支持并发读/写,建议采用 InnoDB 存储引擎
以上就是关于mysql中的锁都有哪些全部的内容,包括:mysql中的锁都有哪些、高并发没锁可不行,三种分布式锁详解、sysbase数据库,行锁,页锁,全页锁定的区别,加锁有什么作用萌新,请大佬们用通俗的话解答一下等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)