Mysql是一个许多人同时使用的存储数据的仓库,所以我们肯定要考虑并发访问时的数据安全问题,一个很有效的办法就是给数据库加上锁,那么在Mysql中有哪些锁呢?什么是并发访问?就是多个人同时访问。
在Mysql中有很多锁,但是不同的锁粒度不同,其中最大的锁能把整个数据库锁上,这个最大的锁有名字吗?这个最大的锁被叫作全局锁,为什么要叫全局锁这个名字呢?为什么不叫库锁?怎么给数据库加上这把锁呢?我们只需要在Mysql的客户端进入数据库后,再执行Flush tables with read lock,就可以给进入的数据库加上一把全局读锁,那么可以给数据库加上全局写锁吗?那么加上这个锁之后会发生什么呢?加上这个锁之后整个数据库都无法再写入数据,无法建表,无法修改已有表的结构,只能从数据库读取数据,所有的增删改语句,建表、修改表结构语句,会更新数据的事务的提交语句都会被阻塞(但是我们可以先开启一个事务,先执行事务中的语句但是不提交,等到全局读锁解除之后再提交),阻塞是什么意思?阻塞指的是在释放这个锁之后会继续执行这些语句。
那么给数据库加上全局读锁有什么用吗?给数据库加全局读锁的主要应用场景是什么?当我们想要对数据库中的数据进行备份时,会先给数据库加上全局读锁,这样在备份的时候数据就不会发生变动了,但是这样做的缺点是什么?如果我们给主数据库加上全局读锁,那么业务就没办法进行了,因为Mysql的主从模式的正常工作状态是主数据库执行增删改语句,从数据库执行查语句,而加上全局读锁后主数据库就不能正常工作了,既然主数据库不能正常工作了,那我们可以把从数据库升级为主数据库,把主数据库降级为从数据库,那此时又会出现给从数据库加全局读锁的问题,如果再给新的数据库加上全局读锁,就会出现和之前相同的问题,如果我们是给从数据库加上全局读锁,那么在备份期间,主从数据库就会出现数据不一致。
既然先给数据库加上全局读锁后再备份会存在各种问题,那如果我们不加锁直接进行备份呢?不加锁直接进行备份会发生什么呢?假设我们是对这么一个数据库进行备份,这个数据库中有两张表,一张表是购买课程表,另一张表是用户余额表,如果是先备份了购买课程表,这时用户购买了一个新的课程,然后再备份用户余额表,此时备份的购买课程表和用户余额表就不匹配了,购买课程表中少了一条记录,但是钱还是正常扣除了,这种情况是用户亏了,如果先备份的是用户余额表,这时用户购买了一个新的课程,然后再备份购买课程表,此时备份的购买课程表和用户余额表同样不匹配,用户余额表中少了一条扣费记录,但是购买的课程却被备份了,相当于白赚了一门课程,这种情况是用户赚了。
从上面的分析可以看到,如果在备份之前不给数据库加锁,就会出现备份中两张表数据不一致的问题。那么对数据库备份是什么意思呢?是怎么对数据库进行备份的呢?
那么有更好的办法对数据库进行备份吗?有的,既然要对数据库进行备份,又要不影响数据库的正常更新,那么我们就要拿到某个时刻数据库的拷贝进行备份,那么怎么拿到某个时刻数据库的拷贝呢?在可重复读隔离级别下开启一个事务就能拿到事务开启时刻的数据库的拷贝,
既然有更好的对数据库进行备份的方法,那么设计出全局读锁还有意义吗?当然有意义,因为不是所有的存储引擎都是支持事务的,比如MyISAM存储引擎,对于不支持事务的存储引擎,只能通过加全局读锁的方法进行备份,而在数据库上加全局读锁又有很多缺点,这也是现在使用Innodb存储引擎更多的原因之一。
将数据库设置为只读模式只有加上全局读锁一种方法吗?当然不是,还有什么办法呢?使用命令set global readonly=true也可以将数据库设置为只读模式,那为什么我们不推荐使用第二种方法来将数据库设置为只读模式呢?主要原因有两个,第一个原因是readonly变量的使用场景比较多,不仅仅是用来把数据库标记为只读模式,比如我们还可以通过readonly变量的值来判断某个库是主库还是备库,那么假如我们只是把主库设置为只读状态,这时就很可能把主库误判为备库,第二个原因是如果在客户端执行的是加全局读锁的命令,由于客户端突然发生了异常导致客户端和服务器之间的连接断开,这时被锁住的数据库会自动退出加锁状态,而如果在客户端执行的是readonly命令,那么即使由于客户端突然发生了异常导致客户端和服务器之间的连接断开了,处于只读模式的数据库也不会主动退出,也就是说,set readonly命令在发生异常后会导致数据库长时间的不可写,相比于使用全局读锁,使用set readonly命令出现问题的可能性更高。由于客户端突然发生了异常导致客户端和服务器之间的连接断开了,这句话能说的具体一点吗?只要客户端发生异常连接就会断开吗?连接断开都是因为客户端发生异常吗?客户端发生异常是什么意思?如果不是由于客户端突然发生了异常导致的连接断开,还会自动退出加锁状态吗?
全局读锁是数据库中粒度最大的锁,那么有没有比它粒度小的锁呢?当然有,我们可以给数据库加上一把锁,自然也可以给数据库的每张表上加锁,此时加上的锁就是表级锁,那么表级锁有哪些类型呢?表级锁的作用会是什么呢?当有线程对该表中的数据进行修改时,阻止别的线程对该表进行操作,那么Mysql设计了哪些表级锁呢?第一种就是表锁,表锁的作用就是在有线程操作表中数据时,阻止别的线程对该表进行操作,但是不限于此,还会限制线程对加锁的表进行何种操作,比如给某张表加的是读锁,那么给表加锁的线程对加锁的表就只能执行读取操作,如果给表加的是写锁,那么给表加锁的线程既可以读取加锁的表,也可以向加锁的表写入数据,而且给表加锁的线程只能操作加锁的表,不能再去操作别的表,那么我们怎么在数据库的某张表上加表锁呢?使用语句lock tables … read/write就可以,那么怎么给某张表解除表锁呢?使用语句unlock tables…就可以。
第二种锁则是元数据锁,元数据锁的作用是防止表的结构被改变,元数据锁不是显式加在表上的,当一个线程在访问某张表时,不管是在对这张表做的是增删改查操作,都会给这张表加上一个元数据读锁,防止在有线程对表访问时会有其它的线程对表的结构进行改变,那么元数据锁有类别的划分吗?有的,元数据锁分为元数据读锁和元数据写锁,当线程对表做的是增删改查操作时,那么线程就会在这张表上加上元数据读锁,当线程对表做的是改变表结构的操作时,那么线程就会在这张表上加上元数据写锁,元数据读锁和元数据读锁是不冲突的,所以多个线程同时对同一张表进行增删改查是被允许的,只要增删改的不是表中的同一行数据都不会出现阻塞(改很容易理解,那么两个线程删除同一行数据会发生什么呢?线程想要删除某个数据行需要先拿到这个数据行的行锁,所以会阻塞其它线程删除这一行数据,其它线程需要等待拿到行锁的线程执行完成,其它线程在数据行被删除后还能够拿到行锁吗?应该不能了,拿不到行锁然后会做什么呢?拿不到行锁存储引擎就会认为数据库中没有这个数据行,直接向客户端返回回复,那么怎么增加同一行数据呢?假设现在有两个线程,有各自需要执行的语句,但是都没有指定主键值,主键值是自增的,那么分别执行这两条语句添加到数据库中的记录的主键值是一致的,那么两个线程增加同一行数据会发生什么呢?主键在自增时是会加锁的(不管是哪个字段,只要它是自增的,在插入新记录时都会加锁,在记录插入成功后才会释放锁),在某个线程插入一条自增记录时其它线程会被阻塞,只有在前一条记录插入成功后,其它线程才能继续插入自增记录),但是元数据读锁和元数据写锁是冲突的,所以在有线程对表进行增删改查时其它线程是不能修改表的结构的。那么在有线程对表进行增删改查时其它线程修改了表的结构会发生什么呢?如果数据库的隔离级别是可重复读,那么和,问题在于一致性视图能够屏蔽表结构的改变吗?
为什么我们只是给一个小表上加一个字段,会导致这个表所在的数据库整个都不可用了?假如我们启动了四个会话,在每个会话的不同时间点执行不同的语句,如下图所示。
四个会话都是在同一个时间点启动的,会话A中的语句最先执行,由于执行的是表t的查询语句,所以会在表t上加上元数据读锁,然后正常执行,会话B中的语句第二个执行,执行的同样是表t的查询语句,所以也会在表t上加上元数据读锁,元数据读锁之间是不冲突的,所以会正常执行,会话C中的语句第三个执行,由于这个语句会改变表的结构,所以在执行之前需要在表上加上元数据写锁,但是此时表上已经加上了元数据读锁,元数据写锁和元数据读锁是冲突的,所以表上是加不上元数据写锁的,也就没办法执行会话C的语句,需要等待会话A和B中的元数据读锁释放才能执行,此时元数据写锁就会添加到等待队列中,由于等待队列是非空的,所以虽然会话D中执行的语句只需要在表上加上元数据读锁,这个元数据读锁也会添加到等待队列中,既然这个元数据读锁没有加到表上,那么这个语句就没办法执行,也就是说会话D中的语句也需要等待会话C的锁释放之后才能执行。这个现象会导致什么样的后果呢?在会话C中语句执行之后才开启的会话中的语句都会被会话C阻塞,不管是增删改查语句,还是修改表结构的语句,想象这么一个场景,各种增删改查语句正在正常执行,然后在某个时刻你开启了一个会话来执行修改表结构的语句,正常的业务场景中应该在开启会话之后就会立马去执行语句,那么就是说在修改表结构的语句执行之后开启的会话都会被阻塞,此时即使是查询功能也不能正常被使用了。
如果在这个业务场景中有着大量的查询需求,而且由于数据库的客户端是具有重试机制的,
为什么不设置为元数据读锁优先加到表上呢?这样做的话如果在修改表结构的语句执行后开启的会话中执行的是增删改查语句就不需要一直等待了,那么这样做有什么缺点呢?缺点在于会导致元数据写锁的饥饿,元数据写锁一直加不到表上。
那么怎么给小表加字段不会出现上面的问题呢?
从上面的过程中可以看到,修改表结构的语句会阻塞在它执行之后开启的会话的直接原因在于修改表结构的语句所在的会话被前面在表上已经加了锁的会话阻塞了,那么我们只需要控制每个会话的存活时间,也就是说我们要避免长事务的出现,及时提交事务,如果事务不提交,事务所在的会话就会一直存在,在表上加的锁就会一直在。那么事务和会话是什么关系呢?如果在你想修改结构的表上现在正在有长事务正在执行,那么最好要么你等长事务执行完了再修改表的结构,要么直接中止这个正在执行的长事务。那么怎么中止正在执行的长事务呢?
那么如果是一个同时有很多长事务的业务场景呢?而我们又必须在这张表上添加一个新的字段,那么这个时候我们怎么做才可以成功在表上添加一个新的字段又不影响到业务的正常进行呢?即使我们中止了当前进行的所有长事务,既然在某个时刻会同时有很多长事务,这说明在这个业务场景中会创建很多的长事务,那么在我们执行修改表结构语句之前也可能又有很多长事务被创建出来了,所以中止正在执行的长事务的方法就没有效果了,也不可能等待长事务执行完再修改表的结构,因为表上一直有长事务在执行,这时我们可以在修改表结构的语句中设置一个等待时间,如果在等待时间内能够在表上加上元数据写锁,那么就执行修改表结构语句,如果在最长等待时间内一直被表上的元数据读锁阻塞,那么就暂时不修改表的结构,等到业务不繁忙的时间段再进行重试。