myDB-day1 TM设计与实现。
本系统架构如下:

系统实现模块顺序:TM->DM->VM->IM->TBM
-
TM是什么?Transactional Manager事务管理器。通过一定的技术手段来管理db的事务信息。
-
transactional 通常有三个状态:0 初始状态/活跃状态 1 提交状态 2 回退状态
-
本项目通过XID设计来实现TM。
- 设计理念:创建一个XID文件以头8个字节 来记录当前文件中有多少个事务,然后指定位置记录当前事务的状态。如图

本文使用到的技术有:NIO(FileChannel,ByteBuffer),枚举,自定义异常,接口默认方法,可重入锁(ReentrantLock)
准备工作:
自定义异常类:CustomerError
/**
* Error
* 用来记录程序运行时的自定义异常
* @author gyg
* @date 2025/6/27
*/
public class CustomerError {
public static final Exception FileNotExistError = new RuntimeException("File does not exists!");
public static final Exception FileExistError = new RuntimeException("File already exists!");
public static Exception FileReadWriteError = new RuntimeException("File cannot read/write!");
//TM
// 无效的XID文件
public static Exception BadXIDFileException = new RuntimeException("Bad XID File!");
}
线程手动停机工具类
/**
* Block
* 系统发生不可忽视异常时,阻断当前线程
* @author gyg
* @date 2025/6/27
*/
public class Block {
public static void block(Exception err) {
err.fillInStackTrace();
System.exit(1);
}
}
randomAccessFile create mode java
randomAccessFile 的打开方式:使用枚举的方式记录一下.
/**
* RandomAccessFileMode
* RandomAccess 访问指定文件的打开方式
* 分为:
* r
* rw
* rwd
* rws
*
* @author gyg
* @date 2025/6/27
*/
@Getter
public enum RandomAccessFileMode {
r("r","只读的方式打开。调用结果对象的任何写入方法都会导致抛出IOException。"),
rw("rw","读写的方式打开。如果文件不存在,则将尝试创建它。"),
rwd("rwd","与“rw”一样,以读写方式打开,并且还要求对文件内容的每次更新都同步写入底层存储设备"),
rws("rws","与“rw”一样,以读写方式打开,并且还要求对文件内容或元数据的每次更新都同步写入底层存储设备。"),
;
private final String mode;
private String remark;
private RandomAccessFileMode(String mode) {
this.mode = mode;
}
private RandomAccessFileMode(String mode,String remark) {
this.mode = mode;
this.remark = remark;
}
}
TransactionManager 事务管理器接口
提供默认方法:create()
/**
* 事务文件的后缀
*/
static final String XID_SUFFIX = ".xid";
// XID文件头长度::用来记录事务个数。然后具体的‘个数偏移量’位置 结合事务占用位数 存放事务的状态。
static final int LEN_XID_HEADER_LENGTH = 8;
// 事务状态占用位数
static final int XID_FIELD_SIZE = 1;
/**
* 创建事务文件:在创建数据库的时候调用。
*
* @param path 文件路径:全路径+文件名(无后缀)
* @return 事务对象 用来处理当前数据库的事务信息。
*/
default TransactionManagerImpl create(String path) {
File file = new File(path + XID_SUFFIX);
try {
if (!file.createNewFile()) {
// 当前创建新文件失败,说明文件已经存在,但是我们是在创建事务,文件常理来说是不存在的,文件存在即说明当前系统出现了不可忽视的异常情况,当中断
Block.block(CustomerError.FileExistError);
}
} catch (IOException e) {
Block.block(e);
}
checkFilePermission(file);
// 使用NIO的方式对文件进行修改 fileChannel是文件读取、写入、映射和操作的通道
FileChannel fc = null;
// 随机访问文件流 RandomAccessFile
RandomAccessFile raf=null;
try {
raf = new RandomAccessFile(file, RandomAccessFileMode.rw.getMode());
fc = raf.getChannel();
} catch (IOException e) {
Block.block(e);
}
// 写 空 XID 文件头 既 0 0 0 0 0 0 0 0
ByteBuffer buffer = ByteBuffer.wrap(new byte[LEN_XID_HEADER_LENGTH]);
try {
assert fc != null;
fc.position(0); // 设置文件通道从文件的0 位置开始.
fc.write(buffer);// 将缓冲区中的内容写入文件通道。
} catch (IOException e) {
Block.block(e);
}
return new TransactionManagerImpl(raf,fc);// 创建或者打开事务文件 返回事务对象
}
/**
* 验证事务文件权限
* @param file
*/
private static void checkFilePermission(File file) {
if (!file.canRead() || !file.canWrite()) {
// 日志文件没有读或者写的权限,表明我们后续记录事务信息就会出错,此处直接中断。
Block.block(CustomerError.FileReadWriteError);
}
}
/**
* 构造函数 返回事务对象。
*
* @param raf
* @param fc
*/
public TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) {
this.file = raf;
this.fc = fc;
// 可重入锁:默认非公平锁
counterLock = new ReentrantLock();
checkXidCounter();
}
提供默认方法 open()
public static TransactionManagerImpl open(String path) {
File f = new File(path+TransactionManagerImpl.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 {
// 获取NIO通道 fc
raf = new RandomAccessFile(f, "rw");
fc = raf.getChannel();
} catch (FileNotFoundException e) {
Panic.panic(e);
}
// 打开事务对象
return new TransactionManagerImpl(raf, fc);
}
提供接口
- begin() 开始一个事务,并返回当前事务的XID[在文件中的位置信息]
- commit() 提交事务
- rollback() 回滚事务
- isActive(Long xid) 指定xid 的事务是否存在
- isCommitted(long xid) 指定xid的事务是否已经提交
- isRollbacked(Long xid)指定xid 的事务是否已经回滚
- close() 关闭事务文件
具体实现:
创建事务文件对象
private RandomAccessFile file;
private FileChannel fc;
// XID 文件计数器:用来判断当前的文件是否合法 并记录事务个数.
private long xidCounter;
private Lock counterLock;
// 事务的三种状态
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 = 0;
/**
* 构造函数 返回事务对象。
*
* @param raf
* @param fc
*/
public TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) {
this.file = raf;
this.fc = fc;
// 可重入锁:默认非公平锁
counterLock = new ReentrantLock();
checkXidCounter();
}
/**
* 检查XID文件是否合法 读取XID_FILE_HEADER 中的 xidCounter,根据它计算文件的理论长度,对比实际长度
*/
public void checkXidCounter() {
long fileLen = 0; // 文件长度字段
try {
fileLen = file.length(); // 获取文件长度
} catch (IOException e) {
Block.block(CustomerError.BadXIDFileException);
}
if (fileLen < LEN_XID_HEADER_LENGTH) {
// LEN_XID_HEADER_LENGTH 是在XID文件创建的时候指定的文件头长度:我们XID文件是 8位文件头和1位数据体(记录事务id)构成的,所以正常的
// XID文件文件长裤绝不会低于LEN_XID_HEADER_LENGTH。
Block.block(CustomerError.BadXIDFileException);
}
// 分配新的字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(LEN_XID_HEADER_LENGTH);
try {
fc.position(0);// 设置通道读取起始位置。
fc.read(buf); // 读取通道内容到缓冲区。
} catch (IOException e) {
Block.block(e);
}
// 获取头文件位置
this.xidCounter = buf.getLong();// 将缓冲区当前位置的八个字节按顺序组成一个long值,然后将位置递增8
// this.xidCounter+1 的原因是:buf 是从00000000开始的 那么数量也是从0 开始计数的。
// 所以算出来为7 实际上应该有8个。
long end = getXidPosition(this.xidCounter + 1);
if (end != fileLen) {
Block.block(CustomerError.BadXIDFileException);
}
}
// 根据事务xid取得其在xid文件中对应的位置
private long getXidPosition(long xid) {
// xid-1 的原因是:
// 1. XID=0 的状态没记;
// 2. 文件的下标是从0 开始的,所以第一个事务的下标应该是8.
return LEN_XID_HEADER_LENGTH + (xid - 1) * XID_FIELD_SIZE;
}
begin:开启一个事务
@Override
public long create() {
// 先加锁
counterLock.lock();
try {
// 事务个数计数器先加一
long xid = xidCounter + 1;
updateXID(xid, FIELD_TRAN_ACTIVE);
incrXIDCounter();
} finally {
// 不管结果如何 解锁
counterLock.unlock();
}
return 0;
}
/**
* 更新xid事务的状态为status
*
* @param xid 事务在文件中id 用来记录对应的偏移量
* @param status 需要填入的状态值
*/
private void updateXID(long xid, byte status) {
// 获取事务真实的偏移量
long offset = getXidPosition(xid);
byte[] tem = new byte[LEN_XID_HEADER_LENGTH];
tem[0] = status;
ByteBuffer wrap = ByteBuffer.wrap(tem);
try {
fc.position(offset); //设置状态写入位置
fc.write(wrap); // 写入文件
fc.force(false); // 强制刷新内容到磁盘
} catch (IOException e) {
Block.block(e);
}
}
/**
* 更新xid计数器和对应文件头信息
*/
private void incrXIDCounter() {
xidCounter++;
ByteBuffer buf = ByteBuffer.allocate(Long.BYTES).putLong(xidCounter);
try {
fc.position(0);
fc.write(buf);
} catch (IOException e) {
Block.block(e);
}
try {
fc.force(false);
} catch (IOException e) {
Block.block(e);
}
}
commit:提交事务
@Override
public void commit(long xid) {
counterLock.lock();
try {
updateXID(xid, FIELD_TRAN_COMMITTED);
} finally {
counterLock.unlock();
}
}
rollback:记录事务回滚
@Override
public void rollback(long xid) {
counterLock.lock();
try {
updateXID(xid, FIELD_TRAN_ABORTED);
} finally {
counterLock.unlock();
}
}
isActive:判断是否活跃
@Override
public Boolean isActive(long xid) {
if (xid == SUPER_XID) {
return false;
}
return checkXIDStatus(xid, FIELD_TRAN_ACTIVE);
}
/**
* 检查xid是否在某个状态
*
* @param xid 事务id
* @param status 状态
* @return
*/
private Boolean checkXIDStatus(long xid, byte status) {
long xidPosition = getXidPosition(xid);
ByteBuffer buf = ByteBuffer.wrap(new byte[XID_FIELD_SIZE]);
try {
fc.position(xidPosition);
fc.read(buf);
} catch (IOException e) {
Block.block(e);
}
return buf.get(0) == status;
}
isCommit:是否提交
@Override
public Boolean isCommitted(long xid) {
if (xid == SUPER_XID) {
return true;
}
return checkXIDStatus(xid, FIELD_TRAN_COMMITTED);
}
isRollback:是否回滚
@Override
public Boolean isRollback(long xid) {
if (xid == SUPER_XID) {
return false;
}
return checkXIDStatus(xid, FIELD_TRAN_ABORTED);
}
关闭事务文件
@Override
public void close() {
try {
fc.close();
file.close();
} catch (IOException e) {
Block.block(e);
}
}
技术探讨:
1. 可重入锁和普通锁的区别
可重入锁:(Reentrant Lock) 和 普通锁的区别在于 同一个线程是否可以重复获取已经持有的锁,规避了嵌套调用 死锁的风险.
| 特性 | 可重入锁 | **普通锁(不可重入锁)** |
|---|---|---|
| 重入性 | 线程持有锁后,可再次获取同一把锁(递归调用或嵌套同步域不会阻塞) | 线程持有锁后,再次请求同一把锁会导致阻塞或死锁 |
| 实现原理 | 通过计数器记录锁的持有次数:获取锁时计数器+1,释放时-1,计数器归零后锁才释放 | 无状态记录,获取锁后直接将锁标记为占用状态,重复请求时判定为已被占用 |
| 死锁风险 | 避免嵌套调用导致的死锁(如递归方法同步) | 嵌套调用同一锁会立即引发死锁 |
| 典型代表 | synchronized 关键字、ReentrantLock 类 | 自定义锁(如简易自旋锁)、ThreadPoolExecutor.Worker 内部类锁 |
| 使用场景 | 递归算法、嵌套同步代码块、复杂对象的多方法同步访问 | 简单同步场景(无需嵌套访问) |
1. 重入机制:
-
可重入锁:线程执行外层同步方法时获取锁,如果内层方法/递归调用时需要同一把锁,可直接进入,然后锁计数器+1.
-
不可重入锁:线程已持有锁时,再调用需同一锁的内层方法会因锁被占用而永久阻塞
-
示例:
// 可重入锁示例(synchronized) public synchronized void outer() { inner(); // 可正常进入,不会死锁 } public synchronized void inner() { /* ... */ } // 不可重入锁模拟(自定义锁) public class NonReentrantLock { private boolean isLocked = false; public synchronized void lock() throws InterruptedException { while (isLocked) wait(); // 若已上锁,再次调用时线程阻塞 isLocked = true; } }
2. 避免自死锁风险
可重入锁的设计消除了 同一线程嵌套请求锁时的自死锁风险,而不可重入锁在此场景下必然死锁.
3. 技术实现
ReentrantLock通过AQS(Abstract Queued Synchronizer)的state字段实现重入计数。synchronized在 JVM 层面通过锁对象头的标记记录重入次数
注:java内置锁 均为可重入锁,非特殊场景一般不需要单独设置不可重入锁
2. 锁引申内容
悲观锁/乐观锁/排他锁(独享锁、互斥锁)/公平锁/非公平锁/重入锁/全局锁/表锁/行锁/意向共享锁/临键锁/记录锁
悲观锁/排他锁/互斥锁/写锁:适用于并发量比较高、对独写资源竞争高得场景。
对于资源竞争呈悲观态度,在获取到资源的时候就会加锁。其他获取资源的线程在这里就会阻塞。
排他锁可以看作是写锁,因为有写入操作,如果多个线程同时进行的话,会有线程安全问题,所以一定是排他的。
// 写锁
ReentrantReadWriteLock.ReadLock readLock = new ReentrantReadWriteLock().readLock();
乐观锁:
对于资源竞争呈乐观态度,在获取到资源的时候不会加锁,而是出现竞争得时候,采用 CAS机制 自旋询问得方式进行等待,直到上一个线程释放资源,
公平锁:
对于一个资源的竞争呈现先到先得的架势,一个线程上锁之后,其他的线程竞争这个资源就需要排队等待,依次获取资源。
非公平锁:
与公平锁不同,他不会遵循先到先得,而是把调度放给CPU,相对于公平锁而言,非公平锁效率更高一些。主要在于上下文切换的时间损耗等。
共享锁:
我们可以把共享锁看成是读锁,因为大家都是读操作,所以不会有线程安全的问题就可以把锁共享出来。
// 读锁
ReentrantReadWriteLock.WriteLock writeLock = new ReentrantReadWriteLock().writeLock();
可重入锁:
同一个线程持有的锁,可以重复持有,不需要等待。释放锁的时候通过锁计数器减一的操作,直到锁计数器为0,整个锁才真正释放。
synchronized 的锁升级制度
无锁->偏向锁(类似可重入锁:偏向第一个获取锁的线程)->轻量级锁(自旋锁:会有cpu资源浪费问题)->重量级锁(应用态切换到内核态)
synchronized 的底层,它是根据并发量的多少,进行升级的,并发越高,锁越重,性能越低,越安全
343

被折叠的 条评论
为什么被折叠?



