[{"data":1,"prerenderedAt":371},["ShallowReactive",2],{"content:\u002Fprojects\u002Fmydb\u002Fmydb6":3,"surround:\u002Fprojects\u002Fmydb\u002Fmydb6":360},{"id":4,"title":5,"body":6,"categories":337,"date":339,"description":340,"draft":341,"extension":342,"image":343,"meta":344,"navigation":346,"path":347,"permalink":343,"published":343,"readingTime":348,"recommend":343,"references":343,"seo":353,"sitemap":354,"stem":355,"tags":356,"type":358,"__hash__":359},"content\u002Fposts\u002Fprojects\u002Fmydb\u002Fmydb6.md","MYDB 6. 记录的版本与事务隔离",{"type":7,"value":8,"toc":323},"minimark",[9,21,25,28,34,37,41,46,49,62,65,73,81,84,95,98,101,104,108,111,114,117,120,129,132,135,138,141,149,152,158,165,171,174,180,187,193,202,205,208,211,214,217,226,229,232,235,241,244,247,253,256,259,262,265,271,274,277,282,285,293,296,299,305,308,314,317],[10,11,12,13,20],"p",{},"本章涉及代码都在 ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fgithub.com\u002FCN-GuoZiyang\u002FMYDB\u002Ftree\u002Fmaster\u002Fsrc\u002Fmain\u002Fjava\u002Ftop\u002Fguoziyang\u002Fmydb\u002Fbackend\u002Fvm",[18],"nofollow","backend\u002Fvm"," 中。",[22,23,24],"h3",{"id":24},"前言",[10,26,27],{},"从这一章开始，我们开始讨论 Version Manager。",[29,30,31],"blockquote",{},[10,32,33],{},"VM 基于两段锁协议实现了调度序列的可串行化，并实现了 MVCC 以消除读写阻塞。同时实现了两种隔离级别。",[10,35,36],{},"类似于 Data Manager 是 MYDB 的数据管理核心，Version Manager 是 MYDB 的事务和数据版本的管理核心。",[22,38,40],{"id":39},"_2pl-与-mvcc","2PL 与 MVCC",[42,43,45],"h4",{"id":44},"冲突与-2pl","冲突与 2PL",[10,47,48],{},"首先来定义数据库的冲突，暂时不考虑插入操作，只看更新操作（U）和读操作（R），两个操作只要满足下面三个条件，就可以说这两个操作相互冲突：",[50,51,52,56,59],"ol",{},[53,54,55],"li",{},"这两个操作是由不同的事务执行的",[53,57,58],{},"这两个操作操作的是同一个数据项",[53,60,61],{},"这两个操作至少有一个是更新操作",[10,63,64],{},"那么这样，对同一个数据操作的冲突，其实就只有下面这两种情况：",[50,66,67,70],{},[53,68,69],{},"两个不同事务的 U 操作冲突",[53,71,72],{},"两个不同事务的 U、R 操作冲突",[10,74,75,76,80],{},"那么冲突或者不冲突，意义何在？作用在于，",[77,78,79],"strong",{},"交换两个互不冲突的操作的顺序，不会对最终的结果造成影响","，而交换两个冲突操作的顺序，则是会有影响的。",[10,82,83],{},"现在我们先抛开冲突不谈，记得在第四章举的例子吗，在并发情况下，两个事务同时操作 x。假设 x 的初值是 0：",[85,86,91],"pre",{"className":87,"code":89,"language":90},[88],"language-text","T1 begin\nT2 begin\nR1(x) \u002F\u002F T1 读到 0\nR2(x) \u002F\u002F T2 读到 0\nU1(0+1) \u002F\u002F T1 尝试把 x+1\nU2(0+1) \u002F\u002F T2 尝试把 x+1\nT1 commit\nT2 commit\n","text",[92,93,89],"code",{"__ignoreMap":94},"",[10,96,97],{},"最后 x 的结果是 1，这个结果显然与期望的不符。",[10,99,100],{},"VM 的一个很重要的职责，就是实现了调度序列的可串行化。MYDB 采用两段锁协议（2PL）来实现。当采用 2PL 时，如果某个事务 i 已经对 x 加锁，且另一个事务 j 也想操作 x，但是这个操作与事务 i 之前的操作相互冲突的话，事务 j 就会被阻塞。譬如，T1 已经因为 U1(x) 锁定了 x，那么 T2 对 x 的读或者写操作都会被阻塞，T2 必须等待 T1 释放掉对 x 的锁。",[10,102,103],{},"由此来看，2PL 确实保证了调度序列的可串行话，但是不可避免地导致了事务间的相互阻塞，甚至可能导致死锁。MYDB 为了提高事务处理的效率，降低阻塞概率，实现了 MVCC。",[42,105,107],{"id":106},"mvcc","MVCC",[10,109,110],{},"在介绍 MVCC 之前，首先明确记录和版本的概念。",[10,112,113],{},"DM 层向上层提供了数据项（Data Item）的概念，VM 通过管理所有的数据项，向上层提供了记录（Entry）的概念。上层模块通过 VM 操作数据的最小单位，就是记录。VM 则在其内部，为每个记录，维护了多个版本（Version）。每当上层模块对某个记录进行修改时，VM 就会为这个记录创建一个新的版本。",[10,115,116],{},"MYDB 通过 MVCC，降低了事务的阻塞概率。譬如，T1 想要更新记录 X 的值，于是 T1 需要首先获取 X 的锁，接着更新，也就是创建了一个新的 X 的版本，假设为 x3。假设 T1 还没有释放 X 的锁时，T2 想要读取 X 的值，这时候就不会阻塞，MYDB 会返回一个较老版本的 X，例如 x2。这样最后执行的结果，就等价于，T2 先执行，T1 后执行，调度序列依然是可串行化的。如果 X 没有一个更老的版本，那只能等待 T1 释放锁了。所以只是降低了概率。",[10,118,119],{},"还记得我们在第四章中，为了保证数据的可恢复，VM 层传递到 DM 的操作序列需要满足以下两个规则：",[29,121,122],{},[10,123,124,125,128],{},"规定 1：正在进行的事务，不会读取其他任何未提交的事务产生的数据。",[126,127],"br",{},"\n规定 2：正在进行的事务，不会修改其他任何未提交的事务修改或产生的数据。",[10,130,131],{},"由于 2PL 和 MVCC，我们可以看到，这两个条件都被很轻易地满足了。",[22,133,134],{"id":134},"记录的实现",[10,136,137],{},"对于一条记录来说，MYDB 使用 Entry 类维护了其结构。虽然理论上，MVCC 实现了多版本，但是在实现中，VM 并没有提供 Update 操作，对于字段的更新操作由后面的表和字段管理（TBM）实现。所以在 VM 的实现中，一条记录只有一个版本。",[10,139,140],{},"一条记录存储在一条 Data Item 中，所以 Entry 中保存一个 DataItem 的引用即可：",[85,142,147],{"className":143,"code":145,"language":146,"meta":94},[144],"language-java","public class Entry {\n    private static final int OF_XMIN = 0;\n    private static final int OF_XMAX = OF_XMIN+8;\n    private static final int OF_DATA = OF_XMAX+8;\n\n    private long uid;\n    private DataItem dataItem;\n    private VersionManager vm;\n\n    public static Entry loadEntry(VersionManager vm, long uid) throws Exception {\n        DataItem di = ((VersionManagerImpl)vm).dm.read(uid);\n        return newEntry(vm, di, uid);\n    }\n\n    public void remove() {\n        dataItem.release();\n    }\n}\n","java",[92,148,145],{"__ignoreMap":94},[10,150,151],{},"我们规定，一条 Entry 中存储的数据格式如下：",[85,153,156],{"className":154,"code":155,"language":90},[88],"[XMIN] [XMAX] [DATA]\n",[92,157,155],{"__ignoreMap":94},[10,159,160,161,164],{},"XMIN 是创建该条记录（版本）的事务编号，而 XMAX 则是删除该条记录（版本）的事务编号。它们的作用将在下一节中说明。DATA 就是这条记录持有的数据。根据这个结构，在创建记录时调用的 ",[92,162,163],{"code":163},"wrapEntryRaw()"," 方法如下：",[85,166,169],{"className":167,"code":168,"language":146,"meta":94},[144],"public static byte[] wrapEntryRaw(long xid, byte[] data) {\n    byte[] xmin = Parser.long2Byte(xid);\n    byte[] xmax = new byte[8];\n    return Bytes.concat(xmin, xmax, data);\n}\n",[92,170,168],{"__ignoreMap":94},[10,172,173],{},"同样，如果要获取记录中持有的数据，也就需要按照这个结构来解析：",[85,175,178],{"className":176,"code":177,"language":146,"meta":94},[144],"\u002F\u002F 以拷贝的形式返回内容\npublic byte[] data() {\n    dataItem.rLock();\n    try {\n        SubArray sa = dataItem.data();\n        byte[] data = new byte[sa.end - sa.start - OF_DATA];\n        System.arraycopy(sa.raw, sa.start+OF_DATA, data, 0, data.length);\n        return data;\n    } finally {\n        dataItem.rUnLock();\n    }\n}\n",[92,179,177],{"__ignoreMap":94},[10,181,182,183,186],{},"这里以拷贝的形式返回数据，如果需要修改的话，需要对 DataItem 执行 ",[92,184,185],{"code":185},"before()"," 方法，这个在设置 XMAX 的值中体现了：",[85,188,191],{"className":189,"code":190,"language":146,"meta":94},[144],"public void setXmax(long xid) {\n    dataItem.before();\n    try {\n        SubArray sa = dataItem.data();\n        System.arraycopy(Parser.long2Byte(xid), 0, sa.raw, sa.start+OF_XMAX, 8);\n    } finally {\n        dataItem.after(xid);\n    }\n}\n",[92,192,190],{"__ignoreMap":94},[10,194,195,197,198,201],{},[92,196,185],{"code":185}," 和 ",[92,199,200],{"code":200},"after()"," 是在 DataItem 一节中就已经确定的数据项修改规则。",[22,203,204],{"id":204},"事务的隔离级别",[42,206,207],{"id":207},"读提交",[10,209,210],{},"上面提到，如果一个记录的最新版本被加锁，当另一个事务想要修改或读取这条记录时，MYDB 就会返回一个较旧的版本的数据。这时就可以认为，最新的被加锁的版本，对于另一个事务来说，是不可见的。于是版本可见性的概念就诞生了。",[10,212,213],{},"版本的可见性与事务的隔离度是相关的。MYDB 支持的最低的事务隔离程度，是“读提交”（Read Committed），即事务在读取数据时，只能读取已经提交事务产生的数据。保证最低的读提交的好处，第四章中已经说明（防止级联回滚与 commit 语义冲突）。",[10,215,216],{},"MYDB 实现读提交，为每个版本维护了两个变量，就是上面提到的 XMIN 和 XMAX：",[218,219,220,223],"ul",{},[53,221,222],{},"XMIN：创建该版本的事务编号",[53,224,225],{},"XMAX：删除该版本的事务编号",[10,227,228],{},"XMIN 应当在版本创建时填写，而 XMAX 则在版本被删除，或者有新版本出现时填写。",[10,230,231],{},"XMAX 这个变量，也就解释了为什么 DM 层不提供删除操作，当想删除一个版本时，只需要设置其 XMAX，这样，这个版本对每一个 XMAX 之后的事务都是不可见的，也就等价于删除了。",[10,233,234],{},"如此，在读提交下，版本对事务的可见性逻辑如下：",[85,236,239],{"className":237,"code":238,"language":90},[88],"(XMIN == Ti and                             \u002F\u002F 由 Ti 创建且\n    XMAX == NULL                            \u002F\u002F 还未被删除\n)\nor                                          \u002F\u002F 或\n(XMIN is commited and                       \u002F\u002F 由一个已提交的事务创建且\n    (XMAX == NULL or                        \u002F\u002F 尚未删除或\n    (XMAX != Ti and XMAX is not commited)   \u002F\u002F 由一个未提交的事务删除\n))\n",[92,240,238],{"__ignoreMap":94},[10,242,243],{},"若条件为 true，则版本对 Ti 可见。那么获取 Ti 适合的版本，只需要从最新版本开始，依次向前检查可见性，如果为 true，就可以直接返回。",[10,245,246],{},"以下方法判断某个记录对事务 t 是否可见：",[85,248,251],{"className":249,"code":250,"language":146,"meta":94},[144],"private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) {\n    long xid = t.xid;\n    long xmin = e.getXmin();\n    long xmax = e.getXmax();\n    if(xmin == xid && xmax == 0) return true;\n\n    if(tm.isCommitted(xmin)) {\n        if(xmax == 0) return true;\n        if(xmax != xid) {\n            if(!tm.isCommitted(xmax)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n",[92,252,250],{"__ignoreMap":94},[10,254,255],{},"这里的 Transaction 结构只提供了一个 XID。",[42,257,258],{"id":258},"可重复读",[10,260,261],{},"读提交会导致的问题大家也都很清楚，八股也背了不少。那就是不可重复读和幻读。这里我们来解决不可重复读的问题。",[10,263,264],{},"不可重复度，会导致一个事务在执行期间对同一个数据项的读取得到不同结果。如下面的结果，加入 X 初始值为 0：",[85,266,269],{"className":267,"code":268,"language":90},[88],"T1 begin\nR1(X) \u002F\u002F T1 读得 0\nT2 begin\nU2(X) \u002F\u002F 将 X 修改为 1\nT2 commit\nR1(X) \u002F\u002F T1 读的 1\n",[92,270,268],{"__ignoreMap":94},[10,272,273],{},"可以看到，T1 两次读 X，读到的结果不一样。如果想要避免这个情况，就需要引入更严格的隔离级别，即可重复读（repeatable read）。",[10,275,276],{},"T1 在第二次读取的时候，读到了已经提交的 T2 修改的值，导致了这个问题。于是我们可以规定：",[29,278,279],{},[10,280,281],{},"事务只能读取它开始时，就已经结束的那些事务产生的数据版本",[10,283,284],{},"这条规定，增加于，事务需要忽略：",[50,286,287,290],{},[53,288,289],{},"在本事务后开始的事务的数据;",[53,291,292],{},"本事务开始时还是 active 状态的事务的数据",[10,294,295],{},"对于第一条，只需要比较事务 ID，即可确定。而对于第二条，则需要在事务 Ti 开始时，记录下当前活跃的所有事务 SP(Ti)，如果记录的某个版本，XMIN 在 SP(Ti) 中，也应当对 Ti 不可见。",[10,297,298],{},"于是，可重复读的判断逻辑如下：",[85,300,303],{"className":301,"code":302,"language":90},[88],"(XMIN == Ti and                 \u002F\u002F 由 Ti 创建且\n (XMAX == NULL                  \u002F\u002F 尚未被删除\n))\nor                              \u002F\u002F 或\n(XMIN is commited and           \u002F\u002F 由一个已提交的事务创建且\n XMIN \u003C XID and                 \u002F\u002F 这个事务小于 Ti 且\n XMIN is not in SP(Ti) and      \u002F\u002F 这个事务在 Ti 开始前提交且\n (XMAX == NULL or               \u002F\u002F 尚未被删除或\n  (XMAX != Ti and               \u002F\u002F 由其他事务删除但是\n   (XMAX is not commited or     \u002F\u002F 这个事务尚未提交或\nXMAX > Ti or                    \u002F\u002F 这个事务在 Ti 开始之后才开始或\nXMAX is in SP(Ti)               \u002F\u002F 这个事务在 Ti 开始前还未提交\n))))\n",[92,304,302],{"__ignoreMap":94},[10,306,307],{},"于是，需要提供一个结构，来抽象一个事务，以保存快照数据：",[85,309,312],{"className":310,"code":311,"language":146,"meta":94},[144],"public class Transaction {\n    public long xid;\n    public int level;\n    public Map\u003CLong, Boolean> snapshot;\n    public Exception err;\n    public boolean autoAborted;\n\n    public static Transaction newTransaction(long xid, int level, Map\u003CLong, Transaction> active) {\n        Transaction t = new Transaction();\n        t.xid = xid;\n        t.level = level;\n        if(level != 0) {\n            t.snapshot = new HashMap\u003C>();\n            for(Long x : active.keySet()) {\n                t.snapshot.put(x, true);\n            }\n        }\n        return t;\n    }\n\n    public boolean isInSnapshot(long xid) {\n        if(xid == TransactionManagerImpl.SUPER_XID) {\n            return false;\n        }\n        return snapshot.containsKey(xid);\n    }\n}\n",[92,313,311],{"__ignoreMap":94},[10,315,316],{},"构造方法中的 active，保存着当前所有 active 的事务。于是，可重复读的隔离级别下，一个版本是否对事务可见的判断如下：",[85,318,321],{"className":319,"code":320,"language":146,"meta":94},[144],"private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {\n    long xid = t.xid;\n    long xmin = e.getXmin();\n    long xmax = e.getXmax();\n    if(xmin == xid && xmax == 0) return true;\n\n    if(tm.isCommitted(xmin) && xmin \u003C xid && !t.isInSnapshot(xmin)) {\n        if(xmax == 0) return true;\n        if(xmax != xid) {\n            if(!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n",[92,322,320],{"__ignoreMap":94},{"title":94,"searchDepth":324,"depth":324,"links":325},4,[326,328,332,333],{"id":24,"depth":327,"text":24},3,{"id":39,"depth":327,"text":40,"children":329},[330,331],{"id":44,"depth":324,"text":45},{"id":106,"depth":324,"text":107},{"id":134,"depth":327,"text":134},{"id":204,"depth":327,"text":204,"children":334},[335,336],{"id":207,"depth":324,"text":207},{"id":258,"depth":324,"text":258},[338],"项目","2021-12-18 14:58:00","VM 通过两段锁协议确保调度序列的可串行化，并引入多版本并发控制（MVCC），以消除读写阻塞问题。此外还定义了数据库操作中的冲突，特别关注更新与读取操作的相互影响，为理解事务间的隔离级别奠定基础。",false,"md",null,{"slots":345},{},true,"\u002Fprojects\u002Fmydb\u002Fmydb6",{"text":349,"minutes":350,"time":351,"words":352},"14 min read",13.915,834900,2783,{"title":5,"description":340},{"loc":347},"posts\u002Fprojects\u002Fmydb\u002Fmydb6",[146,357],"mydb","tech","Br6vm0sHCHuimG4lkqG6P196VZNHXiqxt7YzJP8jQKc",[361,366],{"title":362,"path":363,"stem":364,"date":365,"type":358,"children":-1},"MYDB 5. 页面索引与 DM 的实现","\u002Fprojects\u002Fmydb\u002Fmydb5","posts\u002Fprojects\u002Fmydb\u002Fmydb5","2021-12-11 15:16:00",{"title":367,"path":368,"stem":369,"date":370,"type":358,"children":-1},"MYDB 7. 死锁检测与 VM 的实现","\u002Fprojects\u002Fmydb\u002Fmydb7","posts\u002Fprojects\u002Fmydb\u002Fmydb7","2021-12-23 21:20:00",1787554445745]