XID文件
在数据库中,每一个事务都有一个id编号,用来唯一标识此事务。为了简单起见,假设所有事务从0开始,并自增。我们将所有的事务保存在xid文件中。为了记录事务总数,采用long数据类型进行记录,也就是使用8字节进行记录,所以xid文件的头部,要使用8字节表示总事务数。使用一字节表示每个事务,具体来说就是用一字节表示事务的状态。
数据库中,事务具有三种状态:active(运行状态,使用0表示),committed(提交状态,使用1表示),aborted(回滚状态,使用2表示)。
XID文件内部格式 |
|
值得注意的是,我们在操作MySQL的时候不是都要开启事务的,有些不需要我们开启事务,针对这些不需要开启事务的操作,默认事务id=0。
代码实现
首先定义一些变量
// XID文件头长度,表示总事务数
static final int XID_HEADER_LENGTH = 8;
// XID文件中每个事务状态的长度
private static final int XID_STATE_LENGTH = 1;
// 超级事务,永远为提交状态
public static final long SUPER_XID = 0;
// 事务的三种状态
private static final byte XID_STATE_ACTIVE = 0;
private static final byte XID_STATE_COMMITTED = 1;
private static final byte XID_STATE_ABORTED = 2;
// xid文件后缀
static final String XID_SUFFIX = ".xid";
// 随机接入类,用于写xid文件
private RandomAccessFile file;
// 文件管道
private FileChannel fc;
// xid文件中记录的总事件数
private long xidCounter;
// 使用锁机制来保证线程安全
private Lock counterLock;
开启事务操作:要开启一个事务,首先要定义事务id,记录事务状态,最后更新XID文件中总事务个数。
/**
* 开启事务,并返回事务id
* @return 事务id
*/
@Override
public long begin() {
// 保障原子性
counterLock.lock();
try {
// 获取事务id,并更改状态为运行,同时写入xid文件
long xid = xidCounter + 1;
updateXID(xid, XID_STATE_ACTIVE);
incrXIDCounter();
return xid;
} finally {
counterLock.unlock();
}
}
/**
* 更新xid事务状态,并持久化到xid文件中
* @param xid 事务id
* @param status 事务状态
*/
private void updateXID(long xid, byte status) {
// 获取事务在xid文件中的偏移量
long offset = getXidPosition(xid);
byte[] tmp = new byte[XID_STATE_LENGTH];
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);
}
}
/**
* 事务总数加一,更新文件头部
*/
private void incrXIDCounter() {
xidCounter ++;
// 将总事务数转成8字节数据
ByteBuffer buf = ByteBuffer.wrap(Parser.long2Byte(xidCounter));
try {
fc.position(0);
fc.write(buf);
} catch (IOException e) {
Panic.panic(e);
}
try {
fc.force(false);
} catch (IOException e) {
Panic.panic(e);
}
}
提交事务、回滚事务操作:其实就是将事务状态进行更新
/**
* 提交事务
* @param xid 事务id
*/
@Override
public void commit(long xid) {
updateXID(xid, XID_STATE_COMMITTED);
}
/**
* 回滚事务
* @param xid 事务id
*/
@Override
public void abort(long xid) {
updateXID(xid, XID_STATE_ABORTED);
}
检查事务状态:首先获取事务在xid文件中的相应位置,并读取一字节状态数据,和输入状态进行对比。因为有id=0的操作(不需要事务),所以在判断时需要注意
/**
* 检测事务是否处于指定状态
* @param xid 事务id
* @param status 事务状态
* @return
*/
private boolean checkXID(long xid, byte status) {
long offset = getXidPosition(xid);
// 读物事务一字节状态数据
ByteBuffer buf = ByteBuffer.wrap(new byte[XID_STATE_LENGTH]);
try {
fc.position(offset);
fc.read(buf);
} catch (IOException e) {
Panic.panic(e);
}
return buf.array()[0] == status;
}
/**
* 检查事务是否处于运行状态
* @param xid 事务id
* @return
*/
@Override
public boolean isActive(long xid) {
if(xid == SUPER_XID) return false;
return checkXID(xid, XID_STATE_ACTIVE);
}
/**
* 检查事务是否处于提交状态
* @param xid 事务id
* @return
*/
@Override
public boolean isCommitted(long xid) {
if(xid == SUPER_XID) return true;
return checkXID(xid, XID_STATE_COMMITTED);
}
/**
* 检查事务是否处于回滚状态
* @param xid 事务id
* @return
*/
public boolean isAborted(long xid) {
if(xid == SUPER_XID) return false;
return checkXID(xid, XID_STATE_ABORTED);
}
对于新建的文件对象要检查文件是否出错:判断是否是8字节表示总事务数,换句话就是文件长度必须大于8。然后根据文件长度计算出来的总长度和文件真实长度是否相同。
/**
* 初始化事务文件,传入一个事务文件
* @param raf
* @param fc
*/
public TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) {
this.file = raf;
this.fc = fc;
counterLock = new ReentrantLock();
// 检查文件
checkXIDCounter();
}
/**
* 检查XID文件是否合法,也就是头部用8字节表示总事务
*/
private void checkXIDCounter() {
long fileLen = 0;
try {
fileLen = file.length();
} catch (IOException e) {
Panic.panic(Error.BadXIDFileException);
}
// 文件长度最少都是8字节
if (fileLen < XID_HEADER_LENGTH){
Panic.panic(Error.BadXIDFileException);
}
// 获取前8个字节,计算总事务数
ByteBuffer buf = ByteBuffer.allocate(XID_HEADER_LENGTH);
try {
fc.position(0);
fc.read(buf);
} catch (IOException e) {
Panic.panic(e);
}
this.xidCounter = Parser.parseLong(buf.array());
// 文件实际最大长度
long end = getXidPosition(this.xidCounter + 1);
if(end != fileLen) {
Panic.panic(Error.BadXIDFileException);
}
}
/**
* 根据事务id获取其在文件中的位置
* @param xid 事务的id
* @return 事务对应的位置
*/
private long getXidPosition(long xid) {
return XID_HEADER_LENGTH + (xid - 1) * XID_STATE_LENGTH;
}
真实数据测试
为了更好的理解数据格式,我简单模拟了几个事务,XID文件数据格式如下:
总共有2个事务,事务1回滚,事务2提交。
其余内容可在公众号查看。