mysql判断时间是否在某个区间_MySql那些事⼉(⼋):⼀篇
⽂章搞懂事务的隔离性与锁...
作者:阿茂循环结构的三种语句格式为
从今天开始我们来填之前Mysql系列⽂章的坑,这⼀篇⽂章我们来聊⼀聊Mysql的事务隔离性与隔离级别,这个概念是⾯试基本上涉及到Mysql问题必问的问题。接下来跟我⼀起来梳理下相关概念,让你在⾯试中游刃有余,在平时⼯作中少踩坑。提到事务我就得碎碎念
ACID(Atomicity、Consistency、Isolation、Durability,即原⼦性、⼀致性、隔离性、持久性),⽽这⾥我们说的就是其中的“隔离性”。当我们在同⼀个数据库中同时执⾏多个事务时就会出现:脏读,不可重复读,幻读的问题,为了解决这个问题,数据库中就有了隔离级别的概念。常规的隔离级别包括:读未提交,读提交,可重复读,串⾏化。
读未提交:⼀个事务还没提交时,它做的变更就能被别的事务看到;
读提交:⼀个事务提交之后,它做的变更才会被其他事务看到;
可重复读:⼀个事务执⾏过程中看到的数据,总是跟这个事务在启动时(所有已提交数据)看到的数据是⼀致的;
串⾏化:就是同⼀⾏记录在被事务读写时,通过互斥⽅式保证同⼀时刻只有⼀个事务执⾏,其他事务处于阻塞中。集团公司架构设计
事务隔离的实现与MVCC
mysql面试题库关于事务的隔离性实现,这⾥将通过“可重复读”展开说明。事务在启动的时候会为每⼀个事务创建⼀个read-view(也称之为“事务快照”)并且数据系统会给它分配⼀个全局唯⼀的递增ID称之:transaction id。每个事务来更新这⼀⾏数据时,都会产⽣⼀个数据版本,并且把transaction id赋值给当前数据版
本的事务ID,称之:row trx_id。与之同时回保留旧版本数据⽇志(undo log),在新版本中的
roll_pointer指向旧版本id。也就是说⼀⾏记录可能会有多个版本,每个版本都会有⾃⼰的row trx_id。这⾥说明⼀下,数据版本只是⼀个逻辑概念,⽽不是物理存在的。它可以通过当前版本的undo log依次计算出之前每个数据版本(read-view)的数据与版本信息。我们在本系列⽂章《MySql那些事⼉(五):事务是个什么东西?》中介绍了关于read-view的⼀些属性说明,有了这些概念就很好判断某个版本是否可见:
1. 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它⾃⼰修改过的记录,所以该版本可
以被当前事务访问;
2. 如果被访问版本的trx_id属性值⼩于ReadView中的min_trx_id值,表明该版本⽣成的事务在当前事务⽣成ReadView前已经提交,所
以该版本可以被当前事务访问;
3. 如果被访问版本的trx_id属性值⼤于ReadView中的max_trx_id值,表明⽣成该版本的事务在当前事务⽣成ReadView后才开启,所以
该版本不可以被当前事务访问;
4. 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断⼀下trx_id属性值是不是在m_ids列表
中,如果在,说明创建ReadView时⽣成该版 本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时⽣成成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链到下⼀个版本的数据,继续按照上边的步骤判断可见性。这⾥需要注意点的是,每种隔离事务隔离级别创建⽣成read-view的时机是有所区别的。READ COMMITTD在每次进普通SELECT操作前都会成个ReadView,REPEATABLE READ只在第次进⾏普通SELECT操作前⽣成⼀个ReadView,之后的查询操作都重复使⽤这个ReadView就好了。
总结:执⾏DELETE语句或者更新主键的UPDATE语句并不会⽴即把对应的记录完全从页⾯中删除,⽽是执⾏⼀个所谓的delete mark操作,相当于只是对记录打上了⼀个删除标志位,这主要就是为MVCC服务的。⽽insert语句却不需要⽀持MCVV,所以在事务提交后就被释放掉了。
关于幻读
cellpadding和cellspacing幻读解释过来就是:如果⼀个事务先根据某些条件查询出这些记录,之后另⼀个事务⼜按之前查询条件向表中插⼊了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另⼀个事务插⼊的记录也读出来,那就意味着发⽣了幻读。这⾥我们还是要针对不同隔离级别来说明,在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插⼊的数据的。因此,幻读在“当前读”下才会出现。⼀个事务的结果被另⼀个事务查询到时的当前读,不能称之为幻读,幻读仅指‘新插⼊的⾏’。
关于幻读的问题
哪怕你在update时候将所有数据锁住,也会产⽣幻读,因为你update的只是存在的数据,⽽未来提交的新数据缺不在你的update 范围。幻读的解决
现在你知道了,产⽣幻读的原因是,⾏锁只能锁住⾏,但是新插⼊记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引⼊新的锁,也就是间隙锁(Gap Lock),顾名思义,间隙锁,锁的就是两个值之间的空隙。 假如我有如下这张表
CREATE TABLE `t` (
`id` int(11) NOT NULL,做会计工作学python有什么用
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
初始化插⼊6条记录,就会产⽣7个间隙。当你执⾏ select * from t where d=5 for update的时候,就不⽌是给数据库中已有的6个记录加上了⾏锁,还同时加了7个间隙锁。这样就确保了⽆法再插⼊新的记录。还同时加了7个间隙锁。这样就确保了⽆法再插⼊新的记录。也就是说这时候,在⼀⾏⾏扫描的过程中,不仅将给⾏加上了⾏锁,还给⾏两边的空隙,也加上了间隙锁。我知道⾏锁分读锁和写锁,也就是说跟⾏锁有冲突的关系是另外⼀个⾏的读或写锁。⽽跟间隙锁存在冲突关系的是,在这个间隙范围内的所有操作。间隙锁之间是不存在冲突关系,因为他们不属于同⼀块区间范围。
事务A事务Bbegin;select * from t where c =7 lock in share model;~~begin;select * from t where c =7 for update;
事务A事务B
这⾥session B并不会被堵住。因为表t⾥并没有c=7这个记录,因此session A加的是间隙锁(5,10)。⽽session B也是在这个间隙加的间隙锁。它们有共同的⽬标,即:保护这个间隙,不允许插⼊值。但,它们之间是不冲突的。间隙锁和⾏锁合称next-key lock,每个next-key lock是前开后闭区间。也就是说,我们的表t初始化以后,如果⽤select * from t for update要把整个表所有记录锁起来,就形成了7个next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
在我们引⼊间隙锁和next-key lock 的同时也带来了更复杂的问题,那就是多个事务同时对⼀块没有数据的空间加间隙锁成功后,分别在⾃⼰的事务中操作这块区间的时候尤其是在并发情况下会产⽣死锁,A事务操作区间时发现B事务持有区间间隙锁被阻塞,⽽B事务同时也来操作这块区间时候同样发现A事务持有此区域的间隙锁⽽也被阻塞,导致最终的死锁。
Mysql锁
上⾯我们说到了⼀类锁 ,这⾥我们将常见的数据库锁统⼀讨论⼀下。使⽤锁的场景⼀般就是我们想让⾃⼰写的sql不管在单线程或者在并发中按照我们预想的顺序或者规则来执⾏。根据加锁的范围,MySQL⾥⾯的锁⼤致可以分成全局锁、表级锁和⾏锁三类。
全局锁/实例锁
redis阻塞命令有哪些
全局锁就是对整个数据库实例加锁。MySQL提供了⼀个加全局读锁的⽅法,命令是:Flush tables with read lock。在这之后的所有写操作,包括表结构都会被阻塞。这类锁⼀般⽤于数据库备份(当然这个操作仅限于冷数据操作)。当然我们也可以将数据导出和备份当做⼀个事务启动(–single-transaction),前⾯我们说过事务的视图是由MVCC机制来保证⼀致性的。
设置只读⼤家是不是⼜想到了set global readonly=true;呢?确实将数据库设置成readonly⽅式也可以让全库进⼊只读模式。但是readonly这个在使⽤过程中容易和我们主从同步的业务逻辑产⽣混淆(readonly是标志主库从库的重要标识)。还有个重要的原因就是⼆者的机制是不同的,FTWRL命令执⾏之后于客户端发⽣异常断开,那么MySQL会⾃动释放这个全局锁,readonly是不会的。
表锁
MySQL⾥⾯表级别的锁有两种:⼀种是表锁,⼀种是元数据锁(meta data lock,MDL),也称MLD锁。
表锁
标锁的语法是:lock tables … read/write。它属于FTWRL操作,可以⽤unlock tables主动释放锁,也可以在客户端断开的时候⾃动释放。表锁是⽐较狠的⾓⾊,不但限制别⼈操作表,⽽且还限制⾃⼰后
⾯除本锁以外的操作。如果在某个线程A中执⾏lock tables t1 read, t2 write; 这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执⾏unlock tables之前,也只能执⾏读t1、读写t2的操作。连写t1都不允许,⾃然也不能访问其他表。
元数据锁/MDL锁
MDL锁是不需要显⽰声明,它是访问即加锁的机制,读锁之间不互斥,读锁与写锁之间互斥,写锁与写锁之间互斥,他们⽤来保证表结构变更的操作安全性。这⾥说到表结构变更的安全性,在⽣产上要谨慎使⽤MDL写锁对热点表操作(也就是变更表结构),否则会在短时间阻塞⼤量的数据库请求,当有长事务执⾏时候先Kill掉长事务。否则可能就出现因为你对表结构略微的变更导致数据库崩溃的现象。这⾥建议使⽤‘DDL NOWAIT/WAIT n’这个语法,通过等待空闲时间来变更MDL。如果数据库在很繁忙的时候 可能这个命令可能会失败,但是不会影响正常业务。
⾏锁
以上的锁MyISAM和InnoDB两种引擎都是⽀持的,⽽⾏锁只⽀持InnoDB引擎。他是粒度较⼩的⼀种,也是我们常⽤的锁之⼀。在InnoDB 事务中,⾏锁是在需要的时候才加上的,但并不是不需要了就⽴刻释放,⽽是要等到事务结束时才释放。知道了这个规则也就是说,如果你的事务中需要锁多个⾏,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。举个⽐较简单的例⼦:A,B两个⼈同时在
超市买单,分别减A,B两个⼈账户的钱,然后将钱加到超市的⼀个账户中。这样的场景,我们是将A,B买单的场景分别放在⼀个事务中,那么存在并发的就是超市账户更新的时候。其他操作不存在资源竞争。那么我们就将给超时账户加余额的操作放到每个事务的最后⼀步,这样就能让⾏锁在多个事务中持续时间最短,造成的阻塞时间最短。
使⽤⾏锁也是⽐较容易发⽣死锁的场景,那我们在使⽤⾏锁的时候要做好死锁兜底策略,这⾥我们就先讨论两种:
1:阻塞到超时后⾃动释放,我们可以使⽤innodb_lock_wait_timeout来设置,默认50秒,在设置这个时候的时候 要能让程序明显区分出正常的锁等待和死锁,是个不断尝试与统计数据量状态⽽得出的结果,否则随便设置的话就会导致有些莫名的问题哦。
2:主动发起死锁检测。发现死锁后,主动回滚死锁中的某个事物。将参数innodb_deadlock_detect设置为on,表⽰开启死锁检测。
死锁检测在⾼并发的场景下会因为每个线程的加⼊来判断是否会导致死锁,消耗⼤量的CPU资源。
其实想避免死锁,我们还可以从程序层⾯或者业务设计上避免,双管齐下才保险,不要把所有的问题都扔给数据库,因为基本上数据库是最后⼀层防线,要是最后⼀层防线轻易被击垮的,那就离跑路不远了。
总结
这⼀章我们讲了关于事务隔离性实现的原理与在可重复读级别下产⽣幻读的解决办法,顺带说了下锁的问题,以及它所带来的新的问题。在技术层⾯没有永远的银弹只有更适合我们业务的办法与在不断采坑中凝练出⼀套解决办法。今天关于事务的隔离性问题就讲到这⾥,欢迎⼤家关于此话题
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论