当前位置: 首页 > news >正文

网站的优化与推广h5响应式网站模板制作

网站的优化与推广,h5响应式网站模板制作,滑县网站建设公司,google下载安装说在前面#xff1a;从0开始#xff0c;手写MySQL的学习价值 尼恩曾经指导过的一个7年经验小伙#xff0c;凭借精通Mysql, 搞定月薪40K。 从0开始#xff0c;手写一个MySQL的学习价值在于#xff1a; 可以深入地理解MySQL的内部机制和原理#xff0c;Mysql可谓是面试的…说在前面从0开始手写MySQL的学习价值 尼恩曾经指导过的一个7年经验小伙凭借精通Mysql, 搞定月薪40K。 从0开始手写一个MySQL的学习价值在于 可以深入地理解MySQL的内部机制和原理Mysql可谓是面试的绝对重点和难点从而更好地掌握MySQL的使用和优化。帮助你提高编程能力和解决问题的能力。作为一个优质的简历轮子项目注意这是高质量的简历轮子项目 很多小伙伴的项目都很low极度缺乏轮子项目so轮子项目这就来了。 文章目录 说在前面从0开始手写MySQL的学习价值手写DB架构设计从0开始手写Mysql事务事务5种状态事务5种状态之间的转换 如何管理事务的状态创建xid文件定义常量校验XID文件是否合法开启事务改变事务状态更新xid Header判断事务状态关闭TM 两阶段锁实现事务操作两阶段锁(2PL)功能介绍两阶段锁(2PL)事务实现 手写两阶段锁(2PL)事务事务存储开启事务提交事务回滚事务删除事务插入数据读取事务 事务的ACID特性原子性(Atomicity一致性consistency隔离性isolation持久性durability 死锁死锁检测判断死锁 高并发场景如何做事务隔离事务的隔离级别功能分析读未提交READ UNCOMMITTED读已提交READ COMMITTED可重复读REPEATABLE READ可串行化SERIALIZABLE MVCC并发事务带来的问题读-读写-写写-读或读-写 mvcc 的实现原理隐藏字段ReadView版本链 手写事务隔离级别已提交读可重复读版本跳跃 作者介绍说在后面技术自由的实现路径 PDF实现你的 架构自由实现你的 响应式 自由实现你的 spring cloud 自由实现你的 linux 自由实现你的 网络 自由实现你的 分布式锁 自由实现你的 王者组件 自由实现你的 面试题 自由 手写DB架构设计 尼恩的风格 开始写代码前先做架构 从功能上来说一个手写DB系统架构分为以下几个模块 数据管理器DM事务管理器TM版本管理器VM表管理器TBM索引管理器(IM)、 手写DB架构设计设计图具体如下 从0开始手写Mysql事务 事务是应用程序中一系列严密的操作所有操作必须成功完成否则在每个操作中所作的所有更改都会被撤消。 也就是事务具有原子性一个事务中的一系列的操作要么全部成功要么一个都不做。 事务的结束有两种当事务中的所有步骤全部成功执行时事务提交。 如果其中一个步骤失败将发生回滚操作撤消事务开始时的所有操作。 事务定义的语句如下 (1) BEGIN TRANSACTION事务开始。 (2) END TRANSACTION事务结束。 (3) COMMIT事务提交。该操作表示事务成功地结束它将通知事务管理器该事务的所有更新操作现在可以被提交或永久地保留。 (4) ROLLBACK事务回滚。该操作表示事务非成功地结束它将通知事务管理器出故障了数据库可能处于不一致状态该事务的所有更新操作必须回滚或撤销。 事务5种状态 事务是数据库的基本执行单元如果事务成功执行则数据库从一个一致状态进入另一个一致状态。 事务状态 的状态有以下5种 活动状态事务的初始状态事务执行时处于这个状态。部分提交状态当操作序列的最后一条语句执行后事务就处于部分提交状态。这时事务虽然已经完全执行但由于实际输出可能还临时驻留在内存中在事务成功完成前还有可能出现硬件故障因此部分提交状态并不等于事务成功执行。失败状态由于硬件或逻辑错误使得事务不能继续正常执行事务就进入了失败状态处于失败状态的事务必须回滚。这样事务就进入了中止状态。中止状态事务回滚并且数据库恢复到事务开始执行前的状态。提交状态当事务成功完成后称事务处于提交状态。只有事务处于提交状态后才能说事务已经提交。 如果因为某种原因事务没能成功执行但其已经对数据库进行了修改这时候可能会导致数据库处于不一致的状态需要对事务已经造成的变更进行撤销回滚。 事务5种状态之间的转换 BEGIN TRANSACTION开始运行事务使事务进入活动状态END TRANSACTION说明事物中的所有读写操作都已完成使事务进入部分提交状态把事务的所有操作对数据库的影响存入数据库。COMMIT标志事务已经成功地完成事务中的所有操作对数据库的影响已经安全地存入数据库事务进入提交状态结束事务的运行。ABORT标志事务进入失败状态系统撤销事务中所有操作对数据库和其他事务的影响结束事务的运行。 如何管理事务的状态 在手写数据库 MYDB 中每一个事务都有一个 XID这个 ID 唯一标识了这个事务。 事务的 XID 从 1 开始标号并自增不可重复。 事务管理器TM通过维护XID文件来维护事务的状态并提供接口供其他模块来查询某个事务的状态。 规定 XID 0 是一个超级事务Super Transaction。 当一些操作想在没有申请事务的情况下进行那么可以将操作的 XID 设置为 0。XID 为 0 的事务的状态永远是 committed。 每个事务有三种状态: active正在进行尚未结束,即活动状态committed已提交状态aborted回滚状态 定义如下: // 事务的三种状态 //活动状态 private static final byte FIELD_TRAN_ACTIVE 0; //已提交状态 private static final byte FIELD_TRAN_COMMITTED 1; //回滚状态 private static final byte FIELD_TRAN_ABORTED 2;XID 文件给每个事务分配了一个字节的空间用来保存其状态。 同时在 XID 文件的头部还保存了一个 8 字节的数字记录了这个 XID 文件管理的事务的个数。 于是事务 xid 在文件中的状态就存储在 (xid-1)8 字节处xid-1 是因为 xid 0Super XID 的状态不需要记录。 在TransactionManager中提供了一些接口供其他模块调用,用来创建事务和查询事务状态其接口方法如下: //开启事务 long begin(); //提交事务 void commit(long xid); //撤销事务 void abort(long xid); // 判断事务状态-活动状态 boolean isActive(long xid); // 是否提交状态 boolean isCommitted(long xid); // 是否失败状态 boolean isAborted(long xid); // 关闭事务管理TM void close();创建xid文件 我们需要创建一个xid文件并创建TM对象,其具体实现如下: public static TransactionManagerImpl create(String path) {File f new File(pathTransactionManagerImpl.XID_SUFFIX);try {if(!f.createNewFile()) {Panic.panic(Error.FileExistsException);}} catch (Exception e) {Panic.panic(e);}if(!f.canRead() || !f.canWrite()) {Panic.panic(Error.FileCannotRWException);}FileChannel fc null;RandomAccessFile raf null;try {raf new RandomAccessFile(f, rw);fc raf.getChannel();} catch (FileNotFoundException e) {Panic.panic(e);}// 写空XID文件头ByteBuffer buf ByteBuffer.wrap(new byte[TransactionManagerImpl.LEN_XID_HEADER_LENGTH]);try {//从零创建 XID 文件时需要写一个空的 XID 文件头即设置 xidCounter 为 0// 否则后续在校验时会不合法fc.position(0);fc.write(buf);} catch (IOException e) {Panic.panic(e);}return new TransactionManagerImpl(raf, fc); }//从一个已有的 xid 文件来创建 TM public static TransactionManagerImpl open(String path) {File f new File(pathTransactionManagerImpl.XID_SUFFIX);if(!f.exists()) {Panic.panic(Error.FileNotExistsException);}if(!f.canRead() || !f.canWrite()) {Panic.panic(Error.FileCannotRWException);}FileChannel fc null;RandomAccessFile raf null;try {//用来访问那些保存数据记录的文件raf new RandomAccessFile(f, rw);//返回与这个文件有关的唯一FileChannel对象fc raf.getChannel();} catch (FileNotFoundException e) {Panic.panic(e);}return new TransactionManagerImpl(raf, fc); }定义常量 我们看下TransactionManager接口的实现类TransactionManagerImpl.首先定义一些必要的常量 // XID文件头长度 static final int LEN_XID_HEADER_LENGTH 8; // 每个事务的占用长度 private static final int XID_FIELD_SIZE 1;// 事务的三种状态 //活动状态 private static final byte FIELD_TRAN_ACTIVE 0; //已提交状态 private static final byte FIELD_TRAN_COMMITTED 1; //回滚状态 private static final byte FIELD_TRAN_ABORTED 2;// 超级事务永远为commited状态 public static final long SUPER_XID 1;// XID 文件后缀 static final String XID_SUFFIX .xid;private RandomAccessFile file; private FileChannel fc; private long xidCounter; //显示锁 private Lock counterLock;文件读写都采用了 NIO 方式的 FileChannelFileChannel 提供了一种通过通道来访问文件的方式它可以通过带参数 position(int) 方法定位到文件的任意位置开始进行操作还能够将文件映射到直接内存提高大文件的访问效率。关于java的NIO 请参考java 高并发核心编程(卷1). 校验XID文件是否合法 构造函数创建了一个 TransactionManager 之后首先要对 XID 文件进行校验以保证这是一个合法的 XID 文件。 校验的方式也很简单通过文件头的 8 字节数字反推文件的理论长度与文件的实际长度做对比。如果不同则认为 XID 文件不合法。 TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) {this.file raf;this.fc fc;//显式锁counterLock new ReentrantLock();checkXIDCounter(); }/*** 检查XID文件是否合法* 读取XID_FILE_HEADER中的xidcounter根据它计算文件的理论长度对比实际长度*/ private void checkXIDCounter() {long fileLen 0;try {fileLen file.length();} catch (IOException e1) {Panic.panic(Error.BadXIDFileException);}if(fileLen LEN_XID_HEADER_LENGTH) {//对于校验没有通过的会直接通过 panic 方法强制停机。// 在一些基础模块中出现错误都会如此处理// 无法恢复的错误只能直接停机。Panic.panic(Error.BadXIDFileException);}// java NIO中的Buffer的array()方法在能够读和写之前必须有一个缓冲区// 用静态方法 allocate() 来分配缓冲区ByteBuffer buf ByteBuffer.allocate(LEN_XID_HEADER_LENGTH);try {fc.position(0);fc.read(buf);} catch (IOException e) {Panic.panic(e);}//从文件开头8个字节得到事务的个数this.xidCounter Parser.parseLong(buf.array());// 根据事务xid取得其在xid文件中对应的位置long end getXidPosition(this.xidCounter 1);if(end ! fileLen) {//对于校验没有通过的会直接通过 panic 方法强制停机Panic.panic(Error.BadXIDFileException);} }锁用的是可重入锁ReentrantLock,ReentrantLock是JUC包提供的显式锁的一个基础实现类ReentrantLock类实现了Lock接口它拥有与synchronized相同的并发性和内存语义但是拥有了限时抢占、可中断抢占等一些高级锁特性。关于ReenttrantLock 内容可参考java高并发核心编程(卷2). 文件的理论长度开头的8个字节一个事务状态所占用的字节*事务的个数; 通过xid文件开头的 8 个字节记录了事务的个数反推文件的理论长度与文件的实际长度做对比。如果不等则认为 XID 文件不合法。每次创建xid对象都会检查一次. 对于校验没有通过的会直接通过 panic 方法强制停机。在一些基础模块中出现错误都会如此处理无法恢复的错误只能直接停机。 来获取 xid 的状态在文件中的偏移getXidPosition()方法实现如下: // 根据事务xid取得其在xid文件中对应的位置 private long getXidPosition(long xid) {return LEN_XID_HEADER_LENGTH (xid-1)*XID_FIELD_SIZE; }开启事务 begin() 开启一个事务并初始化事务的结构将其存放在 activeTransaction 中用于检查和快照使用 /***begin() 每开启一个事务并计算当前活跃的事务的结构将其存放在 activeTransaction 中* 用于检查和快照使用* param level* return*/ Override public long begin(int level) {lock.lock();try {long xid tm.begin();//activeTransaction 当前事务创建时活跃的事务,如果level!0,放入t的快照中Transaction t Transaction.newTransaction(xid, level, activeTransaction);activeTransaction.put(xid, t);return xid;} finally {lock.unlock();} }改变事务状态 事务xid的偏移量开头的8个字节一个事务状态所占用的字节*事务的xid; 通过事务xid计算出记录该事务状态的偏移量再更改状态。 具体实现如下: // 更新xid事务的状态为status private void updateXID(long xid, byte status) {long offset getXidPosition(xid);//每个事务占用长度byte[] tmp new byte[XID_FIELD_SIZE];tmp[0] status;ByteBuffer buf ByteBuffer.wrap(tmp);try {fc.position(offset);fc.write(buf);} catch (IOException e) {Panic.panic(e);}try {//将数据刷出到磁盘,但不包括元数据fc.force(false);} catch (IOException e) {Panic.panic(e);} }其中abort()和commit()这是调用了这个方法, // 提交XID事务 public void commit(long xid) {updateXID(xid, FIELD_TRAN_COMMITTED); }// 回滚XID事务 public void abort(long xid) {updateXID(xid, FIELD_TRAN_ABORTED); } 更新xid Header 每次创建一个事务时,都需要将文件头记录的事务个数1; // 将XID加一并更新XID Header private void incrXIDCounter() {xidCounter ;ByteBuffer buf ByteBuffer.wrap(Parser.long2Byte(xidCounter));//游标pos 限制为lim 容量为captry {fc.position(0);fc.write(buf);} catch (IOException e) {Panic.panic(e);}try {fc.force(false);} catch (IOException e) {Panic.panic(e);} }判断事务状态 根据xid-》获取记录事务xid的状态的offset-》读取事务xid的状态-》是否等于status. // 检测XID事务是否处于status状态 private boolean checkXID(long xid, byte status) {long offset getXidPosition(xid);ByteBuffer buf ByteBuffer.wrap(new byte[XID_FIELD_SIZE]);try {fc.position(offset);fc.read(buf);} catch (IOException e) {Panic.panic(e);}return buf.array()[0] status; }// 活动状态判断 public boolean isActive(long xid) {if(xid SUPER_XID) return false;return checkXID(xid, FIELD_TRAN_ACTIVE); }// 已提交状态判断 public boolean isCommitted(long xid) {if(xid SUPER_XID) return true;return checkXID(xid, FIELD_TRAN_COMMITTED); }//回滚状态判断 public boolean isAborted(long xid) {if(xid SUPER_XID) return false;return checkXID(xid, FIELD_TRAN_ABORTED); }关闭TM //TM关闭 public void close() {try {fc.close();file.close();} catch (IOException e) {Panic.panic(e);} }两阶段锁实现事务操作 两阶段锁(2PL)功能介绍 事务调度一般有串行调度和并行调度;首先我们来了解以下几个概念: 并发控制指多用户共享的系统中许多用户可能同时对同一数据进行操作调度事务的执行次序串行调度多个事务依次串行执行且只有当一个事务的所有操作都执行完后才执行另一个事务的所有操作。只要是串行调度执行的结果都是正确的。 并行调度:利用分时的方法同时处理多个事务。但是并行调度的调度结果可能是错误的可能产生不一致的状态包括有丢失修改不可重复读和读脏数据. 在一个事务里面分为加锁(lock)阶段和解锁(unlock)阶段,也即所有的lock操作都在unlock操作之前,如下图所示: 在实际情况下SQL是千变万化、条数不定的,数据库很难在事务中判定什么是加锁阶段什么是解锁阶段。于是引入了S2PL(Strict-2PL),即:在事务中只有提交(commit)或者回滚(rollback)时才是解锁阶段 其余时间为加锁阶段。引入2PL是为了保证事务的隔离性即多个事务在并发的情况下等同于串行的执行。 第一阶段是获得封锁的阶段称为扩展阶段其实也就是该阶段可以进入加锁操作在对任何数据进行读操作之前要申请获得S锁在进行写操作之前要申请并获得X锁加锁不成功则事务进入等待状态直到加锁成功才继续执行。就是加锁后就不能解锁了。 第二阶段是释放封锁的阶段称为收缩阶段当事务释放一个封锁后事务进入封锁阶段在该阶段只能进行解锁而不能再进行加锁操作。 两阶段锁(2PL)事务实现 mysql中事务默认是隐式事务执行insert、update、delete操作的时候数据库自动开启事务、提交或回滚事务。 是否开启隐式事务是由变量autocommit控制的。 所以事务分为隐式事务和显式事务。 隐式事务是由事务自动开启、提交或回滚比如insert、update、delete语句事务的开启、提交或回滚由mysql内部自动控制的。 显式事务是需要手动开启、提交或回滚由开发者自己控制。 手写两阶段锁(2PL)事务 在实现事务隔离级别之前, 我们需要先来讨论一下 Version Manager(VM). VM是基于两阶段锁协议实现了调度序列的可串行化并实现了MVCC 以消除读写阻塞。 同时实现了两种隔离级别.VM 是手写数据库 MYDB的事务和数据版本管理核心. 事务存储 对于一条记录来说MYDB 使用 Entry 类维护了其结构。 虽然理论上MVCC 实现了多版本但是在实现中VM 并没有提供 Update 操作对于字段的更新操作由后面的表和字段管理TBM实现。 所以在 VM 的实现中一条记录只有一个版本。 一条记录存储在一条 Data Item 中所以 Entry 中保存一个 DataItem 的引用即可 public class Entry {private static final int OF_XMIN 0;private static final int OF_XMAX OF_XMIN8;private static final int OF_DATA OF_XMAX8;private long uid;private DataItem dataItem;private VersionManager vm;public static Entry newEntry(VersionManager vm, DataItem dataItem, long uid) {Entry entry new Entry();entry.uid uid;entry.dataItem dataItem;entry.vm vm;return entry;}public static Entry loadEntry(VersionManager vm, long uid) throws Exception {DataItem di ((VersionManagerImpl)vm).dm.read(uid);return newEntry(vm, di, uid);}public void release() {((VersionManagerImpl)vm).releaseEntry(this);}public void remove() {dataItem.release();} }规定一条Entry中存储的数据格式如下: [XMIN] [XMAX] [DATA] 8个字节 8个字节XMIN 是创建该条记录版本的事务编号而 XMAX 则是删除该条记录版本的事务编号。DATA 就是这条记录持有的数据。 根据这个结构在创建记录时调用的 wrapEntryRaw()方法如下 public static byte[] wrapEntryRaw(long xid, byte[] data) {byte[] xmin Parser.long2Byte(xid);byte[] xmax new byte[8];return Bytes.concat(xmin, xmax, data); }同样如果要获取记录中持有的数据也就需要按照这个结构来解析 // 以拷贝的形式返回内容 public byte[] data() {dataItem.rLock();try {SubArray sa dataItem.data();byte[] data new byte[sa.end - sa.start - OF_DATA];System.arraycopy(sa.raw, sa.startOF_DATA, data, 0, data.length);return data;} finally {dataItem.rUnLock();} }这里以拷贝的形式返回数据如果需要修改的话需要遵循一定的流程在修改之前需要调用 before() 方法想要撤销修改时调用 unBefore() 方法在修改完成后调用 after() 方法。 整个流程主要是为了保存前相数据并及时落日志。DM 会保证对 DataItem 的修改是原子性的。 Override public void before() {wLock.lock();pg.setDirty(true);System.arraycopy(raw.raw, raw.start, oldRaw, 0, oldRaw.length); }Override public void unBefore() {System.arraycopy(oldRaw, 0, raw.raw, raw.start, oldRaw.length);wLock.unlock(); }Override public void after(long xid) {dm.logDataItem(xid, this);wLock.unlock(); }在设置 XMAX 的值中体现了如果需要修改的话,要遵循一定的规则, 而且这个版本对每一个XMAX之后的事务都是不可见的,等价于删除了. setXmax代码如下: public void setXmax(long xid) {dataItem.before();try {SubArray sa dataItem.data();System.arraycopy(Parser.long2Byte(xid), 0, sa.raw, sa.startOF_XMAX, 8);} finally {dataItem.after(xid);} }开启事务 begin() 每开启一个事务计算当前活跃的事务的结构将其存放在 activeTransaction 中 Override public long begin(int level) {lock.lock();try {long xid tm.begin();Transaction t Transaction.newTransaction(xid, level, activeTransaction);activeTransaction.put(xid, t);return xid;} finally {lock.unlock();} }提交事务 commit() 方法提交一个事务主要就是 free 掉相关的结构并且释放持有的锁并修改 TM 状态并把该事务从activeTransaction中移除。 Override public void commit(long xid) throws Exception {lock.lock();Transaction t activeTransaction.get(xid);lock.unlock();try {if(t.err ! null) {throw t.err;}} catch(NullPointerException n) {System.out.println(xid);System.out.println(activeTransaction.keySet());Panic.panic(n);}lock.lock();activeTransaction.remove(xid);lock.unlock();lt.remove(xid);tm.commit(xid); }回滚事务 abort 事务的方法则有两种手动和自动。 手动指的是调用 abort() 方法而自动则是在事务被检测出出现死锁时会自动撤销回滚事务或者出现版本跳跃时也会自动回滚 /*** 回滚事务* param xid*/ Override public void abort(long xid) {internAbort(xid, false); }private void internAbort(long xid, boolean autoAborted) {lock.lock();Transaction t activeTransaction.get(xid);//手动回滚if(!autoAborted) {activeTransaction.remove(xid);}lock.unlock();//自动回滚if(t.autoAborted) return;lt.remove(xid);tm.abort(xid); }删除事务 在一个事务 commit 或者 abort 时就可以释放所有它持有的锁并将自身从等待图中删除。 并从等待队列中选择一个xid来占用uid. 解锁时将该 Lock 对象 unlock 即可这样其他业务线程就获取到了锁就可以继续执行了。 public void remove(long xid) {lock.lock();try {ListLong l x2u.get(xid);if(l ! null) {while(l.size() 0) {Long uid l.remove(0);selectNewXID(uid);}}waitU.remove(xid);x2u.remove(xid);waitLock.remove(xid);} finally {lock.unlock();} }// 从等待队列中选择一个xid来占用uid private void selectNewXID(long uid) {u2x.remove(uid);ListLong l wait.get(uid);if(l null) return;assert l.size() 0;while(l.size() 0) {long xid l.remove(0);if(!waitLock.containsKey(xid)) {continue;} else {u2x.put(uid, xid);Lock lo waitLock.remove(xid);waitU.remove(xid);lo.unlock();break;}}if(l.size() 0) wait.remove(uid); }插入数据 insert() 则是将数据包裹成 Entry交给 DM 插入即可 Override public long insert(long xid, byte[] data) throws Exception {lock.lock();Transaction t activeTransaction.get(xid);lock.unlock();if(t.err ! null) {throw t.err;}byte[] raw Entry.wrapEntryRaw(xid, data);return dm.insert(xid, raw); }读取事务 read() 方法读取一个 entry根据隔离级别判断下可见性即可. Override public byte[] read(long xid, long uid) throws Exception {lock.lock();//当前事务xid读取时的快照数据Transaction t activeTransaction.get(xid);lock.unlock();if(t.err ! null) {throw t.err;}Entry entry null;try {//通过uid找要读取的事务dataItementry super.get(uid);} catch(Exception e) {if(e Error.NullEntryException) {return null;} else {throw e;}}try {if(Visibility.isVisible(tm, t, entry)) {return entry.data();} else {return null;}} finally {entry.release();} }事务的ACID特性 事务是访问并更新数据库中各种数据项的一个程序执行单元。事务的目的是要么都修改要么都不做。 大多数场景下 应用都只需要操作单一的数据库这种情况下的事务称之为本地事务Local Transaction。 本地事务的ACID特性是数据库直接提供支持。 为了达成本地事务Mysql 做了很多工作比如回滚日志、重做日志、MVCC 、读写锁等。 对于InnoDB存储引擎而言其默认的事务隔离级别是Repeatable read完全遵循和满足事务的ACID特性。 ACID是原子性(Atomicity、一致性consistency、隔离性isolation、持久性durability 4个单词的缩写。 InnoDB是通过日志和锁来保证事务的ACID特性 通过数据库锁的机制保证事务的隔离性通过Redo Log重做日志来保证事务的隔离性通过Undo Log撤销日志来保证事务的原子性和一致性 原子性(Atomicity 事务必须是一个原子的操作序列单元 事务中包含的各项操作在一次执行过程中要么全部成功要么全部不执行任何一项失败整个事务回滚只有全部都执行成功整个事务才算成功。 在操作任何数据之前 首先将数据备份到Undo Log然后进行数据的修改。如果出现了错误或者用户执行了了Rollback 语句系统可以利用Undo Log 中的备份将数据恢复到事务开始之前的状态以此保证数据的原子性。 一致性consistency 事务的执行不能破坏数据库数据的完整性和一致性事务在执行之前和之后数据库都必须处于一致性状态。 一致性包括两方面的内容分别是约束一致性和数据一致性 约束一致性 创建表结构时所指定的外键、checkmysql不支持、唯一索引等约束。数据一致性 是一个综合性的规定因为它是由原子性、持久性、隔离性共同保证的结果而不是单单依赖于某一种技术。 隔离性isolation 在并发情况中并发的事务是相互隔离的一个事务的执行不能被其他事务干扰。 即不同的事务并发操作相同的数据时每个事务都有各自完整的数据空间即一个事务内部的操作及使用的数据对其他并发事务是个隔离的并发执行的各个事务之间不能相互干扰。 持久性durability 持久性也称为永久性permanence指一个事务一旦提交它对数据库中对应数据的状态变更就应该是永久性的。 即使发生系统崩溃或者机器宕机只要数据库能够重启那么一定能够将其恢复到事务成功结束时的状态。 Redo Log记录的是新数据的备份在事务提交前只要将Redo Log 持久化即可不需要将数据持久化当系统崩溃时虽然数据咩有持久化但是Redo Log 已经持久化。系统可以根据Redo Log的内容将所有数据恢复到崩溃之前的状态这就是使用Redo Log 保障数据的持久化的过程。 总结 数据的完整性主要体现的是一致性特性数据的完整性是通过原子性、隔离性、持久性来保证的这三个特性又是通过redo Log 和Undo Log 来保证的。ACID 特性的关系下图所示 死锁 在mysql中两阶段锁协议(2PL)通常包括扩张和收缩两个阶段。在扩张阶段,事务将获取锁,但不能释放任何锁。在收缩阶段可以释放现有的锁但不能获取新的锁,这种规定存在着死锁的风险。 例如当T1’在扩张阶段获取了Y的读锁并读取了Y此时想要去获取X的写锁却发现T2’的读锁锁定了X而T2’也想要获取Y的写锁。简而言之T1’不得到X是不会释放Y的T2’不得到Y也是不会释放X的这便陷入了循环便形成了死锁。 2PL 会阻塞事务直至持有锁的线程释放锁。可以将这种等待关系抽象成有向边例如Tj 在等待 Ti就可以表示为 Tj -- Ti。这样无数有向边就可以形成一个图。检测死锁只需要查看这个图中是否有环即可。 死锁检测 创建一个LockTable对象在内存中维护这张图维护结构如下: public class LockTable {private MapLong, ListLong x2u; // 某个XID已经获得的资源的UID列表private MapLong, Long u2x; // UID被某个XID持有private MapLong, ListLong wait; // 正在等待UID的XID列表private MapLong, Lock waitLock; // 正在等待资源的XID的锁private MapLong, Long waitU; // XID正在等待的UID...... }在每次出现等待的情况时就尝试向图中增加一条边并进行死锁检测。如果检测到死锁就撤销这条边不允许添加并撤销该事务。 // 不需要等待则返回null否则返回锁对象 // 会造成死锁则抛出异常 public Lock add(long xid, long uid) throws Exception {lock.lock();try {//某个xid已经获得的资源的UID列表如果在这个列表里面则不造成死锁也不需要等待if(isInList(x2u, xid, uid)) {return null;}//表示有了一个新的uid则把uid加入到u2x和x2u里面不死锁不等待// u2x uid被某个xid占有if(!u2x.containsKey(uid)) {u2x.put(uid, xid);putIntoList(x2u, xid, uid);return null;}//以下就是需要等待的情况//多个事务等待一个uid的释放waitU.put(xid, uid);//putIntoList(wait, xid, uid);putIntoList(wait, uid, xid);//造成死锁if(hasDeadLock()) {//从等待列表里面删除waitU.remove(xid);removeFromList(wait, uid, xid);throw Error.DeadlockException;}//从等待列表里面删除Lock l new ReentrantLock();l.lock();waitLock.put(xid, l);return l;} finally {lock.unlock();} }调用 add如果不需要等待则进行下一步如果需要等待的话会返回一个上了锁的 Lock 对象。调用方在获取到该对象时需要尝试获取该对象的锁由此实现阻塞线程的目的 try {l lt.add(xid, uid); } catch(Exception e) {t.err Error.ConcurrentUpdateException;internAbort(xid, true);t.autoAborted true;throw t.err; } if(l ! null) {l.lock();l.unlock(); }判断死锁 查找图中是否有环的算法实际上就是一个深搜需要注意的是这个图不一定是连通图。具体的思路就是为每个节点设置一个访问戳都初始化为 1随后遍历所有节点以每个非 1 的节点作为根进行深搜并将深搜该连通图中遇到的所有节点都设置为同一个数字不同的连通图数字不同。这样如果在遍历某个图时遇到了之前遍历过的节点说明出现了环。 判断死锁的具体实现如下: private boolean hasDeadLock() {xidStamp new HashMap();stamp 1;System.out.println(xid已经持有哪些uid x2ux2u);//xid已经持有哪些uidSystem.out.println(uid正在被哪个xid占用 u2xu2x);//uid正在被哪个xid占用//已经拿到锁的xidfor(long xid : x2u.keySet()) {Integer s xidStamp.get(xid);if(s ! null s 0) {continue;}stamp ;System.out.println(xidxid的stamp是s);System.out.println(进入深搜);if(dfs(xid)) {return true;}}return false; }private boolean dfs(long xid) {Integer stp xidStamp.get(xid);//遍历某个图时遇到了之前遍历过的节点说明出现了环if(stp ! null stp stamp) {return true;}if(stp ! null stp stamp) {return false;}//每个已获得资源的事务一个独特的stampxidStamp.put(xid, stamp);System.out.println(xidStamp找不到该xid加入后xidStamp变为xidStamp);//已获得资源的事务xid正在等待的uidLong uid waitU.get(xid);System.out.println(xidxid正在等待的uid是uid);if(uid null){System.out.println(未成环退出深搜);//xid没有需要等待的uid,无死锁return false;}//xid需要等待的uid被哪个xid占用了Long x u2x.get(uid);System.out.println(xidxid需要的uid被xidx占用了);System.out.println(再次进入深搜xidx);assert x ! null;return dfs(x); }在一个事务 commit 或者 abort 时就可以释放所有它持有的锁并将自身从等待图中删除。 public void remove(long xid) {lock.lock();try {ListLong l x2u.get(xid);if(l ! null) {while(l.size() 0) {Long uid l.remove(0);selectNewXID(uid);}}waitU.remove(xid);x2u.remove(xid);waitLock.remove(xid);} finally {lock.unlock();} }while 循环释放掉了这个线程所有持有的资源的锁这些资源可以被等待的线程所获取 // 从等待队列中选择一个xid来占用uid private void selectNewXID(long uid) {u2x.remove(uid);ListLong l wait.get(uid);if(l null) return;assert l.size() 0;while(l.size() 0) {long xid l.remove(0);if(!waitLock.containsKey(xid)) {continue;} else {u2x.put(uid, xid);Lock lo waitLock.remove(xid);waitU.remove(xid);lo.unlock();break;}}if(l.size() 0) wait.remove(uid); }从 List 开头开始尝试解锁还是个公平锁。解锁时将该 Lock 对象 unlock 即可这样业务线程就获取到了锁就可以继续执行了。 测试代码如下: public static void main(String[] args) throws Exception {LockTable lock new LockTable();lock.add(1L,3L);lock.add(2L,4L);lock.add(3L,5L);lock.add(1L,4L);System.out.println();lock.add(2L,5L);System.out.println();lock.add(3L,3L);System.out.println(lock.hasDeadLock()); }执行结果如下: xid已经持有哪些uid x2u{1[3], 2[4], 3[5]} uid正在被哪个xid占用 u2x{31, 42, 53} xid1的stamp是null 进入深搜 xidStamp找不到该xid加入后xidStamp变为{12} xid1正在等待的uid是4 xid1需要的uid被xid2占用了 再次进入深搜xid2 xidStamp找不到该xid加入后xidStamp变为{12, 22} xid2正在等待的uid是null 未成环退出深搜 xid3的stamp是null 进入深搜 xidStamp找不到该xid加入后xidStamp变为{12, 22, 33} xid3正在等待的uid是null 未成环退出深搜xid已经持有哪些uid x2u{1[3], 2[4], 3[5]} uid正在被哪个xid占用 u2x{31, 42, 53} xid1的stamp是null 进入深搜 xidStamp找不到该xid加入后xidStamp变为{12} xid1正在等待的uid是4 xid1需要的uid被xid2占用了 再次进入深搜xid2 xidStamp找不到该xid加入后xidStamp变为{12, 22} xid2正在等待的uid是5 xid2需要的uid被xid3占用了 再次进入深搜xid3 xidStamp找不到该xid加入后xidStamp变为{12, 22, 32} xid3正在等待的uid是null 未成环退出深搜xid已经持有哪些uid x2u{1[3], 2[4], 3[5]} uid正在被哪个xid占用 u2x{31, 42, 53} xid1的stamp是null 进入深搜 xidStamp找不到该xid加入后xidStamp变为{12} xid1正在等待的uid是4 xid1需要的uid被xid2占用了 再次进入深搜xid2 xidStamp找不到该xid加入后xidStamp变为{12, 22} xid2正在等待的uid是5 xid2需要的uid被xid3占用了 再次进入深搜xid3 xidStamp找不到该xid加入后xidStamp变为{12, 22, 32} xid3正在等待的uid是3 xid3需要的uid被xid1占用了 再次进入深搜xid1高并发场景如何做事务隔离 事务的隔离级别功能分析 锁和多版本控制MVCC技术就是用于保障隔离性的 InnoDB支持的隔离性有4种从低到高分别是 读未提交READ UNCOMMITTED读已提交READ COMMITTED可重复读REPEATABLE READ可串行化SERIALIZABLE。 读未提交READ UNCOMMITTED 在读未提交隔离级别下允许脏读的情况发生。未提交意味着这些数据可能会回滚读到了不一定最终存在的数据。读未提交隔离级别容易出现脏读问题; 脏读指的就是在不同的事务下当前事务可以读到另外事务未提交的数据简单说就是读到脏数据。 而所谓的脏数据是指事务对缓冲池中行记录的修改并且还没有被提交。 如果读到了脏数据即一个事务可以读到另一个事务中未提交的数据显然违反了数据库的隔离性。 脏读发生的条件是需要事务的隔离级别为Read uncommitted 在生产环境中绝大部分数据库至少会设置成Read Committed 所以在生产环境下发送的概率很小。 读已提交READ COMMITTED 只允许读到已提交的数据 即事务A在将n从0累加到10的过程中 B无法看到n的中国值只能看到10. 在已提交隔离级别下禁止了脏读 但是运行不可重复读的情况发生。 读已提交满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变. oracle 、sql server 默认的隔离级别是读已提交。 读已提交隔离级别容易出现不可重复读问题.不可重复读是指一个事务内多次读取同一数据集合的数据不一样的情况。 事务A 多次读取同一数据但事务B在事务A多次读取的过程中对数据作了更新并比较导致事务A多次读取同一数据时结果不一致了。 脏读和不可重复读的区别是脏读读到的是未提交的数据而不可重复读读到的确实已提交的数据但是违反了数据库事务一致性原则。 在一般情况下不可重复读的问题是可以接受的因为其读到的是已经提交的数据 本身不会带来很大的问题。Oracle、SQL server 数据库事务的默认隔离级别设置为Read CommittedRC 隔离级别是允许不可重复读的现象。Mysql 默认的隔离级别是RR 通过使用Next-Key Lock 算法来避免不可重复读的现象。 不可重复读和幻读的区别是 不可重复读的重点在修改: 同样的条件第一次读取过的数据再次读取出来发现值不一样了。幻读的重点在新增或删除同样的条件第一次和第二次读取出来的记录数不一样。 可重复读REPEATABLE READ 保证在事务过程中多次读取同一数据时其值都和事务开始时刻时是一致的。 在可重复读隔离级别下禁止脏读和不可重复读但是存在幻读。 幻读是指一个事务的两次查询中数据笔数不一致例如有一个事务查询了几列(Row)数据而另一个事务却在此时插入了新的几列数据先前的事务在接下来的查询中就会发现有几列数据是它先前所没有的。 mysql 默认的隔离级别是 可重复读查看mysql 当前数据库的事务隔离级别命令如下 show variables like tx_isolation; 或 select tx_isolation;设置事务隔离级别 set tx_isolationREAD-UNCOMMITTED; set tx_isolationREAD-COMMITTED; set tx_isolationREPEATABLE-READ; set tx_isolationSERIALIZABLE;可重复读在理论上,会导致另一个棘手的问题:幻读. 幻读至当前用户读取某一范围的数据行时 ,另一个事务又在该范围内插入新行,当用户再读取该范围的数据行时 ,会发现有新的幻影行. InnoDB存储引擎通过多版本并发控制MVCCMultiversion Concurrency Control机制解决了该问题。 可串行化SERIALIZABLE 最严格的事务要求所有事务被串行执行并不能并发执行. 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题. 换句话说就是在每个读的数据行上加上共享锁. 可串行可能导致大量的超时现象和锁竞争. 总结 隔离级别脏读Dirty Read不可重复读NoRepeatable Read幻读Phantom Read读未提交Read uncommitted可能可能可能读已提交(Read committed不可能可能可能可重复读Repeatable read不可能不可能可能可串行化Serializable不可能不可能不可能 MVCC MVCC全称是Multi-Version Concurrent Control即多版本并发控制,其原理与copyonwrite类似。 并发事务带来的问题 读-读 即并发事务相继读取同一记录. 因为读取记录并不会对记录造成任何影响所以同个事务并发读取同一记录也就不存在任何安全问题所以允许这种操作。不存在任何问题也不需要并发控制. 写-写 即并发事务相继对同一记录做出修改. 如果允许并发事务都读取同一记录并相继基于旧估对这一记录做出修改那么就会出现前一个事务所做的修改被后面事务的修改覆盖即出现提交覆盖的问题。 另外一种情况并发事务相继对同一记录做出修改其中一个事务提交之后之后另一个事务发生回滚这样就会出现已提交的修改因为回滚而丢失的问题即回滚覆盖问题. 所以是存在线程安全问题的,两种情况都可能存在更新丢失问题. 写-读或读-写 即两个并发事务对同一记录分别进行读操作和写操作。 如果一个事务读取了另一个事务尚未提交的修政记录那么就出现了脏读的问题 如果我们加以控制使得一个事务只能读取其他已提交事务的修改的数据那么这个事务在另一个事务提交修改前后读取到的数据是不一样的这就意味看发生了不可重复读 如果一个事务根据一些条件查询到一些记录之后另一事物向表中插入了一些记录原先的事务以相同条件再次查询时发现得到的结果跟第一次查词得到的结果不一致这就意味着发生了幻读。 针对写-读或读-写出现的问题,MySQL的InnoDB实现了MVCC来更好地处理读写冲突可以做到即使存在并发读写也可以不用加锁实现非阻塞并发读。 那么,总结一下,需要MVCC的原因是由于数据库通常使用锁来实现隔离性,最原生的锁,锁住一个资源后会禁止其他任何线程访问统一资源。但是很多应用的一个特点是读多写少的场景很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要.所以就使用了一种读写锁的方法读锁和读锁之间不互斥而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。 之后人们发现并发读还是不够又提出了能不能让读写之间也不冲突的方法就是读取数据时通过一种类似快照的方式将数据保存下来这样读锁就和写锁不冲突了不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型不同的数据库可能用不同的方式来实现这种功能。 在MVCC协议下每个读操作, 会看到一个一致性的snapshot 快照并且可以实现非阻塞的读。读这个snapshot 快照不用加锁。除了snapshot 快照版本之外 MVCC允许数据具有多个版本版本编号可以是时间戳或者是全局递 增的事务ID在同一个时间点不同的事务看到的数据是不同的。 首先,了解MVCC 之前,我们需要明确两个定义 当前读读取的数据是最新版本读取数据时还要保证其他并发事务不会修改当前的数据当前读会对读取的记录加锁。比如select …… lock in share mode共享锁、select …… for update | update | insert | delete排他锁快照读每一次修改数据都会在 undo log 中存有快照记录这里的快照就是读取undo log中的某一版本的快照。这种方式的优点是可以不用加锁就可以读取到数据缺点是读取到的数据可能不是最新的版本。一般的查询都是快照读比如select * from t_user where id1; 在MVCC中的查询都是快照度。 mvcc 的实现原理 MySQL中MVCC主要是通过行记录中的隐藏字段隐藏主键 row_id、事务ID trx_id、回滚指针 roll_pointer、undo log版本链、ReadView一致性读视图来实现的。 隐藏字段 MySQL中在每一行记录中除了自定义的字段还有一些隐藏字段 row_id当数据库表没定义主键时InnoDB会以row_id为主键生成一个聚集索引。trx_id事务ID记录了新增/最近修改这条记录的事务id事务id是自增的。roll_pointer回滚指针指向当前记录的上一个版本在 undo log 中。 ReadView ReadView一致性视图主要是由两部分组成所有未提交事务的ID数组和已经创建的最大事务ID组成。比如[100,200],300。事务100和200是当前未提交的事务而事务300是当前创建的最大事务已经提交了。当执行SELECT语句的时候会创建ReadView但是在读取已提交和可重复读两个事务级别下生成ReadView的策略是不一样的读取已提交级别是每执行一次SELECT语句就会重新生成一份ReadView而可重复读级别是只会在第一次SELECT语句执行的时候会生成一份后续的SELECT语句会沿用之前生成的ReadView即使后面有更新语句的话也会继续沿用。 ReadView 就是MVCC在对数据进行快照读时会产生的一个读视图。ReadView中有4个比较重要的变量 m_ids活跃事务id列表当前系统中所有活跃的也就是没提交的事务的事务id列表。min_trx_idm_ids 中最小的事务id。max_trx_id生成 ReadView 时系统应该分配给下一个事务的id注意不是 m_ids 中最大的事务id也就是m_ids 中的最大事务id 1 。creator_trx_id生成该 ReadView 的事务的事务id。 版本链 修改数据的时候会向 redo log 中记录修改的页内容为了在数据库宕机重启后恢复对数据库的操作也会向 undo log 记录数据原来的快照用于回滚事务。undo log有两个作用除了用于回滚事务还用于实现MVCC。 用一个简单的例子来画一下MVCC中用到的undo log版本链的逻辑图 当事务100trx_id100执行了 insert into t_user values(1,‘弯弯’,30); 之后 当事务102trx_id102执行了 update t_user set name‘李四’ where id1; 之后 当事务102trx_id102执行了 update t_user set name‘王五’ where id1; 之后 而具体版本链的比对规则如下首先从版本链中拿出最上面第一个版本的事务ID开始逐个往下进行比对 其中min_id指向ReadView中未提交事务数组中的最小事务ID而max_id指向ReadView中的已经创建的最大事务ID 某个事务进行快照读时可以读到哪个版本的数据ReadView 的规则如下 1当版本链中记录的 trx_id 等于当前事务idtrx_id creator_trx_id时说明版本链中的这个版本是当前事务修改的所以该快照记录对当前事务可见。 2当版本链中记录的 trx_id 小于活跃事务的最小idtrx_id min_trx_id时说明版本链中的这条记录已经提交了所以该快照记录对当前事务可见。 3当版本链中记录的 trx_id 大于下一个要分配的事务idtrx_id max_trx_id时该快照记录对当前事务不可见。 4当版本链中记录的 trx_id 大于等于最小活跃事务id且版本链中记录的trx_id小于下一个要分配的事务idmin_trx_id trx_id max_trx_id时如果版本链中记录的 trx_id 在活跃事务id列表 m_ids 中说明生成 ReadView 时修改记录的事务还没提交所以该快照记录对当前事务不可见否则该快照记录对当前事务可见。 当事务对 id1 的记录进行快照读时select * from t_user where id1在版本链的快照中从最新的一条记录开始依次判断这4个条件直到某一版本的快照对当前事务可见否则继续比较上一个版本的记录。 MVCC主要是用来解决RU隔离级别下的脏读和RC隔离级别下的不可重复读的问题所以MVCC只在RC解决脏读和RR解决不可重复读隔离级别下生效也就是MySQL只会在RC和RR隔离级别下的快照读时才会生成ReadView。 区别就是在RC隔离级别下每一次快照读都会生成一个最新的ReadView在RR隔离级别下只有事务中第一次快照读会生成ReadView之后的快照读都使用第一次生成的ReadView。 MySQL 通过 MVCC降低了事务的阻塞概率。 例如T1 想要更新记录 X 的值于是 T1 需要首先获取 X 的锁接着更新也就是创建了一个新的 X 的版本假设为 x3。 假设 T1 还没有释放 X 的锁时T2 想要读取 X 的值这时候就不会阻塞MYDB 会返回一个较老版本的 X例如 x2。这样最后执行的结果就等价于T2 先执行T1 后执行调度序列依然是可串行化的。如果 X 没有一个更老的版本那只能等待 T1 释放锁了。 所以只是降低了概率。 手写事务隔离级别 如果一个记录的最新版本被加锁当另一个事务想要修改或读取这条记录时MYDB 就会返回一个较旧的版本的数据。 这时就可以认为最新的被加锁的版本对于另一个事务来说是不可见的。 已提交读 即事务在读取数据时, 只能读取已经提交事务产生的数据。 版本的可见性与事务的隔离度是相关的。 MYDB 支持的最低的事务隔离程度是“读提交”Read Committed即事务在读取数据时, 只能读取已经提交事务产生的数据。保证最低的读提交的好处第四章中已经说明防止级联回滚与 commit 语义冲突。 MYDB 实现读提交为每个版本维护了两个变量就是前面提到的 XMIN 和 XMAX XMIN创建该版本的事务编号XMAX删除该版本的事务编号 XMIN 应当在版本创建时填写而 XMAX 则在版本被删除或者有新版本出现时填写。 XMAX 这个变量也就解释了为什么 DM 层不提供删除操作当想删除一个版本时只需要设置其 XMAX这样这个版本对每一个 XMAX 之后的事务都是不可见的也就等价于删除了。 在读提交下版本对事务的可见性逻辑如下 (XMIN Ti and // 由Ti创建且XMAX NULL // 还未被删除 ) or // 或 (XMIN is commited and // 由一个已提交的事务创建且(XMAX NULL or // 尚未删除或(XMAX ! Ti and XMAX is not commited) // 由一个未提交的事务删除 ))若条件为 true则版本对 Ti 可见。那么获取 Ti 适合的版本只需要从最新版本开始依次向前检查可见性如果为 true就可以直接返回。 以下方法判断某个记录对事务 t 是否可见 private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) {long xid t.xid;long xmin e.getXmin();long xmax e.getXmax();if(xmin xid xmax 0) return true;if(tm.isCommitted(xmin)) {if(xmax 0) return true;if(xmax ! xid) {if(!tm.isCommitted(xmax)) {return true;}}}return false; }可重复读 不可重复度会导致一个事务在执行期间对同一个数据项的读取得到不同结果。如下面的结果加入 X 初始值为 0 T1 begin R1(X) // T1 读得 0 T2 begin U2(X) // 将 X 修改为 1 T2 commit R1(X) // T1 读的 可以看到T1 两次读 X读到的结果不一样。如果想要避免这个情况就需要引入更严格的隔离级别即可重复读. T1 在第二次读取的时候读到了已经提交的 T2 修改的值这个修改事务为原版本的XMAX为新版本的XMIN导致了这个问题。于是我们可以规定事务只能读取它开始时,就已经结束的那些事务产生的数据版本. 这条规定,事务需要忽略2点: (1) 在本事务后开始的事务的数据; (2) 本事务开始时还是active状态的事务的数据. 对于第一条只需要比较事务 ID即可确定。而对于第二条则需要在事务 Ti 开始时记录下当前活跃的所有事务 SP(Ti)如果记录的某个版本XMIN 在 SP(Ti) 中也应当对 Ti 不可见。 于是可重复读的判断逻辑如下 (XMIN Ti and // 由Ti创建且(XMAX NULL or // 尚未被删除 )) or // 或 (XMIN is commited and // 由一个已提交的事务创建且XMIN XID and // 这个事务小于Ti且XMIN is not in SP(Ti) and // 这个事务在Ti开始前提交且(XMAX NULL or // 尚未被删除或(XMAX ! Ti and // 由其他事务删除但是(XMAX is not commited or // 这个事务尚未提交或 XMAX Ti or // 这个事务在Ti开始之后才开始或 XMAX is in SP(Ti) // 这个事务在Ti开始前还未提交 ))))于是需要提供一个结构来抽象一个事务以保存快照数据该事务创建时还活跃着的事务 // vm对一个事务的抽象 public class Transaction {public long xid;public int level;public MapLong, Boolean snapshot;public Exception err;public boolean autoAborted;//事务id 隔离级别 快照public static Transaction newTransaction(long xid, int level, MapLong, Transaction active) {Transaction t new Transaction();t.xid xid;t.level level;if(level ! 0) {//隔离级别为可重复读,读已提交不需要快照信息t.snapshot new HashMap();for(Long x : active.keySet()) {t.snapshot.put(x, true);}}return t;}public boolean isInSnapshot(long xid) {if(xid TransactionManagerImpl.SUPER_XID) {return false;}return snapshot.containsKey(xid);} }构造方法中的 active保存着当前所有 active 的事务。 于是可重复读的隔离级别下一个版本是否对事务可见的判断如下 private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {long xid t.xid;long xmin e.getXmin();long xmax e.getXmax();if(xmin xid xmax 0) return true;if(tm.isCommitted(xmin) xmin xid !t.isInSnapshot(xmin)) {if(xmax 0) return true;if(xmax ! xid) {if(!tm.isCommitted(xmax) || xmax xid || t.isInSnapshot(xmax)) {return true;}}}return false; }版本跳跃 版本跳跃问题考虑如下的情况假设 X 最初只有 x0 版本T1 和 T2 都是可重复读的隔离级别 T1 begin T2 begin R1(X) // T1读取x0 R2(X) // T2读取x0 U1(X) // T1将X更新到x1 T1 commit U2(X) // T2将X更新到x2 T2 commit这种情况实际运行起来是没问题的但是逻辑上不太正确。 T1 将 X 从 x0 更新为了 x1这是没错的。但是 T2 则是将 X 从 x0 更新成了 x2跳过了 x1 版本。 读提交是允许版本跳跃的而可重复读则是不允许版本跳跃的。 解决版本跳跃的思路如果 Ti 需要修改 X而 X 已经被 Ti 不可见的事务 Tj 修改了那么要求 Ti 回滚。 MVCC 的实现使得撤销或是回滚事务时只需要将这个事务标记为 aborted 即可。 根据前一章提到的可见性每个事务都只能看到其他 committed 的事务所产生的数据一个 aborted 事务产生的数据就不会对其他事务产生任何影响了也就相当于这个事务不曾存在过 if(Visibility.isVersionSkip(tm, t, entry)) {System.out.println(检查到版本跳跃自动回滚);t.err Error.ConcurrentUpdateException;internAbort(xid, true);t.autoAborted true;throw t.err; }前面我们总结了,Ti 不可见的Tj,有两种情况: (1) XID(Tj) XID(Ti) 修改版本在Ti之后创建 (2) Tj in SP(Ti) Ti创建时修改版本已经创建但是还未提交 版本跳跃的检查首先取出要修改数据X的最新提交版本,并检查该最新版本的创建者对当前事务是否可见.具体实现如下: public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) {long xmax e.getXmax();if(t.level 0) {return false;} else {return tm.isCommitted(xmax) (xmax t.xid || t.isInSnapshot(xmax));} }作者介绍 一作Mark 资深大数据架构师、Java架构师近20年Java、大数据架构和开发经验。资深架构导师成功指导了多个中级Java、高级Java转型架构师岗位。 二作尼恩资深系统架构师、IT领域资深作家、著名博主。近20年高性能Web平台、高性能通信、高性能搜索、数据挖掘等领域的3高架构研究、系统架构、系统分析、核心代码开发工作。资深架构导师成功指导了多个中级Java、高级Java转型架构师岗位。 说在后面 持续迭代、持续升级 是 尼恩团队的宗旨 持续迭代、持续升级 也是 《从0开始手写MySQL》的灵魂。 后面会收集更多的面试真题同时遇到面试难题可以来尼恩的社区《技术自由圈原疯狂创客圈》中交流和求助。 咱们的目标打造宇宙最牛的《手写MySQL》面试宝典。 技术自由的实现路径 PDF 实现你的 架构自由 《吃透8图1模板人人可以做架构》 《10Wqps评论中台如何架构B站是这么做的》 《阿里二面千万级、亿级数据如何性能优化 教科书级 答案来了》 《峰值21WQps、亿级DAU小游戏《羊了个羊》是怎么架构的》 《100亿级订单怎么调度来一个大厂的极品方案》 《2个大厂 100亿级 超大流量 红包 架构方案》 … 更多架构文章正在添加中 实现你的 响应式 自由 《响应式圣经10W字实现Spring响应式编程自由》 这是老版本 《Flux、Mono、Reactor 实战史上最全》 实现你的 spring cloud 自由 《Spring cloud Alibaba 学习圣经》 《分库分表 Sharding-JDBC 底层原理、核心实战史上最全》 《一文搞定SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系史上最全》 实现你的 linux 自由 《Linux命令大全2W多字一次实现Linux自由》 实现你的 网络 自由 《TCP协议详解 (史上最全)》 《网络三张表ARP表, MAC表, 路由表实现你的网络自由》 实现你的 分布式锁 自由 《Redis分布式锁图解 - 秒懂 - 史上最全》 《Zookeeper 分布式锁 - 图解 - 秒懂》 实现你的 王者组件 自由 《队列之王 Disruptor 原理、架构、源码 一文穿透》 《缓存之王Caffeine 源码、架构、原理史上最全10W字 超级长文》 《缓存之王Caffeine 的使用史上最全》 《Java Agent 探针、字节码增强 ByteBuddy史上最全》 实现你的 面试题 自由 4000页《尼恩Java面试宝典 》 40个专题 尼恩 架构笔记、面试题 的PDF文件更新请到下面《技术自由圈》公号取↓↓↓
http://www.w-s-a.com/news/233419/

相关文章:

  • 电商网站主题photolux wordpress
  • 周口专业做网站公司深圳市宝安区松岗街道邮政编码
  • 上海企业网站推广方法网络营销策划方案框架
  • 一流的常州网站建设机械加工网报价
  • 上海响应式网站建设公司seo课程总结
  • vs网站开发教程昆山普立斯特做的有网站
  • 柳州网站seo网站swordpress 输出内容
  • 网站设计制作电话多少网站流量下降
  • 沈阳做网站推广的公司唐山哪家做网站好
  • 国外著名网站建设公司WordPress破解怎样主题修复
  • 网站建设济南云畅网络广州电力建设有限公司网站
  • 查看公司信息的网站思特奇是外包公司吗
  • 制作企业网站的目的啥都能看的浏览器
  • 做网站可以用哪些语言如何进行网站运营与规划
  • 做效果图网站有哪些电子商城网站制作数据库
  • 小刘网站建设wordpress调用php文件上传
  • 建设银行对账网站网络营销广告案例
  • 做网站开票是多少个点的票wordpress扫码提交数据库
  • 织梦网站改版需要怎么做企业网站备案管理系统
  • 大规模网站开发语言宁夏建设职业技术学院网站
  • 寻花问柳专注做一家男人爱的网站北京展台设计制作
  • 中卫网站设计做自己的卡盟网站
  • 广州网站推广自助做网站人家直接百度能搜到的
  • 电子商务网站建设目标及利益分析安徽建设厅网站施
  • 制作网站策划书网站建设公司的性质
  • 哪个网站可以做免费宣传简单的网页设计网站
  • 福州专业网站制作公司金湖建设局网站
  • 好的移动端网站模板下载兰州线上广告推广
  • 宁波高端建站深圳品牌营销策划机构
  • 权威网站优化价格建设厅科技中心网站首页