一、前言
前面分析了FileSnap,接著繼續分析FileTxnSnapLog原始碼,其封裝了TxnLog和SnapShot,其在持久化過程中是一個幫助類。
二、FileTxnSnapLog原始碼分析
2.1 類的屬性
public class FileTxnSnapLog { //the direcotry containing the //the transaction logs // 日誌檔案目錄 private final File dataDir; //the directory containing the //the snapshot directory // 快照檔案目錄 private final File snapDir; // 事務日誌 private TxnLog txnLog; // 快照 private SnapShot snapLog; // 版本號 public final static int VERSION = 2; // 版本 public final static String version = "version-"; // Logger private static final Logger LOG = LoggerFactory.getLogger(FileTxnSnapLog.class); }
說明:類的屬性中包含了TxnLog和SnapShot介面,即對FileTxnSnapLog的很多操作都會轉發給TxnLog和SnapLog進行操作,這是一種典型的組合方法。
2.2 內部類
FileTxnSnapLog包含了PlayBackListener內部類,用來接收事務應用過程中的回撥,在Zookeeper資料恢復後期,會有事務修正過程,此過程會回撥PlayBackListener來進行對應的資料修正。其原始碼如下
public interface PlayBackListener { void onTxnLoaded(TxnHeader hdr, Record rec); }
說明:在完成事務操作後,會呼叫到onTxnLoaded方法進行相應的處理。
2.3 建構函式
FileTxnSnapLog的建構函式如下
public FileTxnSnapLog(File dataDir, File snapDir) throws IOException { LOG.debug("Opening datadir:{} snapDir:{}", dataDir, snapDir); // 在datadir和snapdir下生成version-2目錄 this.dataDir = new File(dataDir, version + VERSION); this.snapDir = new File(snapDir, version + VERSION); if (!this.dataDir.exists()) { // datadir存在但無法建立目錄,則丟擲異常 if (!this.dataDir.mkdirs()) { throw new IOException("Unable to create data directory " + this.dataDir); } } if (!this.snapDir.exists()) { // snapdir存在但無法建立目錄,則丟擲異常 if (!this.snapDir.mkdirs()) { throw new IOException("Unable to create snap directory " + this.snapDir); } } // 給屬性賦值 txnLog = new FileTxnLog(this.dataDir); snapLog = new FileSnap(this.snapDir); }
說明:對於建構函式而言,其會在傳入的datadir和snapdir目錄下新生成version-2的目錄,並且會判斷目錄是否建立成功,之後會建立txnLog和snapLog。
2.4 核心函式分析
1. restore函式
public long restore(DataTree dt, Map<Long, Integer> sessions, PlayBackListener listener) throws IOException { // 根據snap檔案反序列化dt和sessions snapLog.deserialize(dt, sessions); // FileTxnLog txnLog = new FileTxnLog(dataDir); // 獲取比最後處理的zxid+1大的log檔案的迭代器 TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1); // 最大的zxid long highestZxid = dt.lastProcessedZxid; TxnHeader hdr; try { while (true) { // iterator points to // the first valid txn when initialized // itr在read函式呼叫後就已經指向第一個合法的事務 // 獲取事務頭 hdr = itr.getHeader(); if (hdr == null) { // 事務頭為空 //empty logs // 表示日誌檔案為空 return dt.lastProcessedZxid; } if (hdr.getZxid() < highestZxid && highestZxid != 0) { // 事務頭的zxid小於snapshot中的最大zxid並且其不為0,則會報錯 LOG.error("{}(higestZxid) > {}(next log) for type {}", new Object[] { highestZxid, hdr.getZxid(), hdr.getType() }); } else { // 重新賦值highestZxid highestZxid = hdr.getZxid(); } try { // 在datatree上處理事務 processTransaction(hdr,dt,sessions, itr.getTxn()); } catch(KeeperException.NoNodeException e) { throw new IOException("Failed to process transaction type: " + hdr.getType() + " error: " + e.getMessage(), e); } // 每處理完一個事務都會進行回撥 listener.onTxnLoaded(hdr, itr.getTxn()); if (!itr.next()) // 已無事務,跳出迴圈 break; } } finally { if (itr != null) { // 迭代器不為空,則關閉 itr.close(); } } // 返回最高的zxid return highestZxid; }
說明:restore用於恢復datatree和sessions,其步驟大致如下
① 根據snapshot檔案反序列化datatree和sessions,進入②
② 獲取比snapshot檔案中的zxid+1大的log檔案的迭代器,以對log檔案中的事務進行迭代,進入③
③ 迭代log檔案的每個事務,並且將該事務應用在datatree中,同時會呼叫onTxnLoaded函式進行後續處理,進入④
④ 關閉迭代器,返回log檔案中最後一個事務的zxid(作為最高的zxid)
其中會呼叫到FileTxnLog的read函式,read函式在FileTxnLog中已經進行過分析,會呼叫processTransaction函式,其原始碼如下
public void processTransaction(TxnHeader hdr,DataTree dt, Map<Long, Integer> sessions, Record txn) throws KeeperException.NoNodeException { // 事務處理結果 ProcessTxnResult rc; switch (hdr.getType()) { // 確定事務型別 case OpCode.createSession: // 建立會話 // 新增進會話 sessions.put(hdr.getClientId(), ((CreateSessionTxn) txn).getTimeOut()); if (LOG.isTraceEnabled()) { ZooTrace.logTraceMessage(LOG,ZooTrace.SESSION_TRACE_MASK, "playLog --- create session in log: 0x" + Long.toHexString(hdr.getClientId()) + " with timeout: " + ((CreateSessionTxn) txn).getTimeOut()); } // give dataTree a chance to sync its lastProcessedZxid // 處理事務 rc = dt.processTxn(hdr, txn); break; case OpCode.closeSession: // 關閉會話 // 會話中移除 sessions.remove(hdr.getClientId()); if (LOG.isTraceEnabled()) { ZooTrace.logTraceMessage(LOG,ZooTrace.SESSION_TRACE_MASK, "playLog --- close session in log: 0x" + Long.toHexString(hdr.getClientId())); } // 處理事務 rc = dt.processTxn(hdr, txn); break; default: // 處理事務 rc = dt.processTxn(hdr, txn); } /** * Snapshots are lazily created. So when a snapshot is in progress, * there is a chance for later transactions to make into the * snapshot. Then when the snapshot is restored, NONODE/NODEEXISTS * errors could occur. It should be safe to ignore these. */ if (rc.err != Code.OK.intValue()) { // 忽略處理結果中可能出現的錯誤 LOG.debug("Ignoring processTxn failure hdr:" + hdr.getType() + ", error: " + rc.err + ", path: " + rc.path); } }
說明:processTransaction會根據事務頭中記錄的事務型別(createSession、closeSession、其他型別)來進行相應的操作,對於createSession型別而言,其會將會話和超時時間新增至會話map中,對於closeSession而言,會話map會根據客戶端的id號刪除其會話,同時,所有的操作都會呼叫到dt.processTxn函式,其原始碼如下
public ProcessTxnResult processTxn(TxnHeader header, Record txn) { // 事務處理結果 ProcessTxnResult rc = new ProcessTxnResult(); try { // 從事務頭中解析出相應屬性並儲存至rc中 rc.clientId = header.getClientId(); rc.cxid = header.getCxid(); rc.zxid = header.getZxid(); rc.type = header.getType(); rc.err = 0; rc.multiResult = null; switch (header.getType()) { // 確定事務型別 case OpCode.create: // 建立結點 // 顯示轉化 CreateTxn createTxn = (CreateTxn) txn; // 獲取建立結點路徑 rc.path = createTxn.getPath(); // 建立結點 createNode( createTxn.getPath(), createTxn.getData(), createTxn.getAcl(), createTxn.getEphemeral() ? header.getClientId() : 0, createTxn.getParentCVersion(), header.getZxid(), header.getTime()); break; case OpCode.delete: // 刪除結點 // 顯示轉化 DeleteTxn deleteTxn = (DeleteTxn) txn; // 獲取刪除結點路徑 rc.path = deleteTxn.getPath(); // 刪除結點 deleteNode(deleteTxn.getPath(), header.getZxid()); break; case OpCode.setData: // 寫入資料 // 顯示轉化 SetDataTxn setDataTxn = (SetDataTxn) txn; // 獲取寫入資料結點路徑 rc.path = setDataTxn.getPath(); // 寫入資料 rc.stat = setData(setDataTxn.getPath(), setDataTxn .getData(), setDataTxn.getVersion(), header .getZxid(), header.getTime()); break; case OpCode.setACL: // 設定ACL // 顯示轉化 SetACLTxn setACLTxn = (SetACLTxn) txn; // 獲取路徑 rc.path = setACLTxn.getPath(); // 設定ACL rc.stat = setACL(setACLTxn.getPath(), setACLTxn.getAcl(), setACLTxn.getVersion()); break; case OpCode.closeSession: // 關閉會話 // 關閉會話 killSession(header.getClientId(), header.getZxid()); break; case OpCode.error: // 錯誤 // 顯示轉化 ErrorTxn errTxn = (ErrorTxn) txn; // 記錄錯誤 rc.err = errTxn.getErr(); break; case OpCode.check: // 檢查 // 顯示轉化 CheckVersionTxn checkTxn = (CheckVersionTxn) txn; // 獲取路徑 rc.path = checkTxn.getPath(); break; case OpCode.multi: // 多個事務 // 顯示轉化 MultiTxn multiTxn = (MultiTxn) txn ; // 獲取事務列表 List<Txn> txns = multiTxn.getTxns(); rc.multiResult = new ArrayList<ProcessTxnResult>(); boolean failed = false; for (Txn subtxn : txns) { // 遍歷事務列表 if (subtxn.getType() == OpCode.error) { failed = true; break; } } boolean post_failed = false; for (Txn subtxn : txns) { // 遍歷事務列表,確定每個事務型別並進行相應操作 // 處理事務的資料 ByteBuffer bb = ByteBuffer.wrap(subtxn.getData()); Record record = null; switch (subtxn.getType()) { case OpCode.create: record = new CreateTxn(); break; case OpCode.delete: record = new DeleteTxn(); break; case OpCode.setData: record = new SetDataTxn(); break; case OpCode.error: record = new ErrorTxn(); post_failed = true; break; case OpCode.check: record = new CheckVersionTxn(); break; default: throw new IOException("Invalid type of op: " + subtxn.getType()); } assert(record != null); // 將bytebuffer轉化為record(初始化record的相關屬性) ByteBufferInputStream.byteBuffer2Record(bb, record); if (failed && subtxn.getType() != OpCode.error){ // 失敗並且不為error型別 int ec = post_failed ? Code.RUNTIMEINCONSISTENCY.intValue() : Code.OK.intValue(); subtxn.setType(OpCode.error); record = new ErrorTxn(ec); } if (failed) { // 失敗 assert(subtxn.getType() == OpCode.error) ; } // 生成事務頭 TxnHeader subHdr = new TxnHeader(header.getClientId(), header.getCxid(), header.getZxid(), header.getTime(), subtxn.getType()); // 遞迴呼叫處理事務 ProcessTxnResult subRc = processTxn(subHdr, record); // 儲存處理結果 rc.multiResult.add(subRc); if (subRc.err != 0 && rc.err == 0) { rc.err = subRc.err ; } } break; } } catch (KeeperException e) { if (LOG.isDebugEnabled()) { LOG.debug("Failed: " + header + ":" + txn, e); } rc.err = e.code().intValue(); } catch (IOException e) { if (LOG.isDebugEnabled()) { LOG.debug("Failed: " + header + ":" + txn, e); } } /* * A snapshot might be in progress while we are modifying the data * tree. If we set lastProcessedZxid prior to making corresponding * change to the tree, then the zxid associated with the snapshot * file will be ahead of its contents. Thus, while restoring from * the snapshot, the restore method will not apply the transaction * for zxid associated with the snapshot file, since the restore * method assumes that transaction to be present in the snapshot. * * To avoid this, we first apply the transaction and then modify * lastProcessedZxid. During restore, we correctly handle the * case where the snapshot contains data ahead of the zxid associated * with the file. */ // 事務處理結果中儲存的zxid大於已經被處理的最大的zxid,則重新賦值 if (rc.zxid > lastProcessedZxid) { lastProcessedZxid = rc.zxid; } /* * Snapshots are taken lazily. It can happen that the child * znodes of a parent are created after the parent * is serialized. Therefore, while replaying logs during restore, a * create might fail because the node was already * created. * * After seeing this failure, we should increment * the cversion of the parent znode since the parent was serialized * before its children. * * Note, such failures on DT should be seen only during * restore. */ if (header.getType() == OpCode.create && rc.err == Code.NODEEXISTS.intValue()) { // 處理在恢復資料過程中的結點建立操作 LOG.debug("Adjusting parent cversion for Txn: " + header.getType() + " path:" + rc.path + " err: " + rc.err); int lastSlash = rc.path.lastIndexOf('/'); String parentName = rc.path.substring(0, lastSlash); CreateTxn cTxn = (CreateTxn)txn; try { setCversionPzxid(parentName, cTxn.getParentCVersion(), header.getZxid()); } catch (KeeperException.NoNodeException e) { LOG.error("Failed to set parent cversion for: " + parentName, e); rc.err = e.code().intValue(); } } else if (rc.err != Code.OK.intValue()) { LOG.debug("Ignoring processTxn failure hdr: " + header.getType() + " : error: " + rc.err); } return rc; }
說明:processTxn用於處理事務,即將事務操作應用到DataTree記憶體資料庫中,以恢復成最新的資料。
2. save函式
public void save(DataTree dataTree, ConcurrentHashMap<Long, Integer> sessionsWithTimeouts) throws IOException { // 獲取最後處理的zxid long lastZxid = dataTree.lastProcessedZxid; // 生成snapshot檔案 File snapshotFile = new File(snapDir, Util.makeSnapshotName(lastZxid)); LOG.info("Snapshotting: 0x{} to {}", Long.toHexString(lastZxid), snapshotFile); // 序列化datatree、sessionsWithTimeouts至snapshot檔案 snapLog.serialize(dataTree, sessionsWithTimeouts, snapshotFile); }
說明:save函式用於將sessions和datatree儲存至snapshot檔案中,其大致步驟如下
① 獲取記憶體資料庫中已經處理的最新的zxid,進入②
② 根據zxid和快照目錄生成snapshot檔案,進入③
③ 將datatree(記憶體資料庫)、sessionsWithTimeouts序列化至快照檔案。
其他的函式或多或少都是呼叫TxnLog和SnapLog中的相應函式,之前已經進行過分析,這裡不再累贅。
三、總結
本篇博文分析了FileTxnSnapLog的原始碼,其主要封裝了TxnLog和SnapLog來進行相應的處理,其提供了從snapshot檔案和log檔案中恢復記憶體資料庫的介面,原始碼相對而言較為簡單,也謝謝各位園友的觀看~