模拟数据库事务实现转账
模拟数据库事务实现转账
实现思路
采用和真实数据库同样的策略,来实现数据库的事务。采用redo 和undo 来确保事务的持久性和原子性。
redo log是为了确保事务的持久性,数据库在进行数据的更新操作时,不一定会把更新的结果,立即写入磁盘。如果在更新过的数据(脏页)还没写入磁盘的阶段出现故障.数据库可以利用redo log来将之前未持久化到磁盘的数据持久化到数据库。.
undo log是为了确保事务的原子性,数据库在修改数据之前,均利用undo log进行了数据的备份如果在多次操作之间 数据库故障,可以利用undo恢复原先的数据.
真实的数据库实现A向B转账50的过程(假设两人账户都有100)
1. 事务开始
2. 记录A=100到undo log
3. 修改A=50
4. 记录A=50到 redo log
5. 记录B=100到 undo log
6. 修改B=50
7. 记录B=50到redo log
8. 将redo log写入磁盘
9. 事务提交
代码
import com.sun.org.apache.regexp.internal.REUtil;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class TransactionDemo {
private static final String USER_A ="A.txt";
private static final String USER_B ="B.txt";
private static final String UNDO_LOG_FILE_PATH ="undolog.txt";
private static final String REDO_LOG_FILE_PATH ="redolog.txt";
//用户A和B的账户中的数据
private static int userAaccount;
private static int userBaccount;
public static void main(String[] args) throws IOException, InterruptedException {
//事务的回滚
if(!roleback()){
System.out.println("输入y继续转账,输入n结束转账");
String input = new Scanner(System.in).next();
if("n".equals(input.toLowerCase())){
return;
}
System.out.println("继续转账咯");
}
//执行转账逻辑
execute();
System.out.println("转账成功");
}
/**
* 回滚操作
* @return 执行了回滚返回false,否则则反之
* @throws IOException
*/
private static boolean roleback() throws IOException {
String undo=FileUtil.getFileInfo(UNDO_LOG_FILE_PATH);
if(!"".equals(undo)){
System.out.println("上次事务中断,开始回滚");
Map<String, String> info = getInfoFromLog(UNDO_LOG_FILE_PATH);
persist(info);
clearLogs();
return false;
}
String redo=FileUtil.getFileInfo(REDO_LOG_FILE_PATH);
if(!"".equals(redo)){
System.out.println("上次事务未能持久化到磁盘,开始持久化");
Map<String, String> info = getInfoFromLog(REDO_LOG_FILE_PATH);
persist(info);
clearLogs();
return false;
}
clearLogs();
return true;
}
/**
* 事务开始时的初始化操作
*/
private static void init() throws IOException {
//清空redo 和undo
FileUtil.writeToFile("", REDO_LOG_FILE_PATH);
FileUtil.writeToFile("", UNDO_LOG_FILE_PATH);
loader();
}
/**
* 执行转账事务的方法
*
*
* 基本原理:
* redo log是为了确保事务的持久性,数据库在进行数据的更新操作时,不一定会把
* 更新的结果,立即写入磁盘。如果在更新过的数据(脏页)还没写入磁盘的阶段出现故障
* 数据库可以利用redo log来将之前未持久化到磁盘的数据持久化到数据库。
*
* undo log是为了确保事务的原子性,数据库在修改数据之前,均利用undo log进行了数据的备份
* 如果在操作之间 数据库故障,可以利用undo恢复原先的数据
* @throws IOException
* @throws InterruptedException
*/
private static void execute() throws IOException, InterruptedException {
//做准备工作
init();
//操作用户A的账户数据之前将原先的数据持久化到undolog
FileUtil.addInfoToFile(" A:"+userAaccount,UNDO_LOG_FILE_PATH);
//修改用户A的账户
userAaccount-=50;
//将用户A修改后的账户信息持久化到redo log
FileUtil.addInfoToFile(" A:"+userAaccount,REDO_LOG_FILE_PATH);
//将用户B的账号数据备份到undo log
FileUtil.addInfoToFile(" B:"+userBaccount,UNDO_LOG_FILE_PATH);
//修改用户B的账户
userBaccount+=50;
//将用户B修改后的账户信息持久化到redo log
FileUtil.addInfoToFile(" B:"+userBaccount,REDO_LOG_FILE_PATH);
//能执行到这,所有的更新后的数据以及持久化到redo log了,事务的原子性保障到了清除undo log
FileUtil.writeToFile("", UNDO_LOG_FILE_PATH);
//将redo log中的数据写入磁盘,完成数据持久化
Map<String,String> infos=getInfoFromLog(REDO_LOG_FILE_PATH);
persist(infos);
//能进行到这一步,持久性已经保证了,没必要保留redo,清除
FileUtil.writeToFile("",REDO_LOG_FILE_PATH);
}
/**
* 将用户A和B的账户数据读入内存
* 模拟数据库将数据页加入缓冲池
*/
private static void loader() throws IOException{
userAaccount=Integer.parseInt(FileUtil.getFileInfo(USER_A));
userBaccount=Integer.parseInt(FileUtil.getFileInfo(USER_B));
}
/**
* 从log中解析数据,并以map的方式返回,方便查询
* @param path
* @return
*/
private static Map<String,String> getInfoFromLog(String path) throws IOException {
Map<String,String> result=new HashMap<String,String>();
String info=FileUtil.getFileInfo(path);
String[] infos = info.trim().split(" ");
for(String str:infos){
String[] items=str.split(":");
result.put(items[0],items[1]);
}
return result;
}
/**
* 将账户余额信息持久化到各自的账户文件
* @param infos
*/
private static void persist(Map<String,String> infos) throws IOException {
if(infos.containsKey("A")){
FileUtil.writeToFile(infos.get("A"),USER_A);
}
if(infos.containsKey("B")){
FileUtil.writeToFile(infos.get("B"),USER_B);
}
}
/**
* 清除所有的日志
* @throws IOException
*/
private static void clearLogs() throws IOException {
FileUtil.writeToFile("",UNDO_LOG_FILE_PATH);
FileUtil.writeToFile("",UNDO_LOG_FILE_PATH);
}
}
class FileUtil{
/**
* 读取文件中的内容
* @param path 路径
* @return
* @throws IOException
*/
public static String getFileInfo(String path) throws IOException {
byte[] bytes=new byte[1024];
ByteBuffer buffer=ByteBuffer.wrap(bytes);
FileInputStream stream = new FileInputStream(path);
FileChannel channel = stream.getChannel();
channel.read(buffer);
return new String(bytes,"utf-8").trim();
}
/**
* 向文件中写入内容
* @param info 待写入的内容
* @param path 路径
* @throws IOException
*/
public static void writeToFile(String info,String path) throws IOException {
ByteBuffer buffer=ByteBuffer.allocate(1024);
FileOutputStream stream = new FileOutputStream(path);
buffer.put(info.getBytes("utf-8"));
buffer.flip();
FileChannel channel = stream.getChannel();
channel.write(buffer);
}
/**
* 向文件中追加内容
* @param Info 待追加的内容
* @param path 路径
* @throws IOException
*/
public static void addInfoToFile(String Info,String path) throws IOException {
String oldInfo=getFileInfo(path);
writeToFile(oldInfo+Info,path);
}
}