├── src ├── main │ ├── resources │ │ ├── application-prod.yml │ │ ├── application-dev.yml │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── demo.5889606d.css │ │ │ ├── demo.5889606d.css.map │ │ │ ├── runtime.b0a62080.js │ │ │ ├── index.html │ │ │ └── runtime.b0a62080.js.map │ │ ├── application.yml │ │ └── logback-spring.xml │ └── java │ │ └── com │ │ └── dyx │ │ └── simpledb │ │ ├── backend │ │ ├── parser │ │ │ ├── statement │ │ │ │ ├── Abort.java │ │ │ │ ├── Commit.java │ │ │ │ ├── Show.java │ │ │ │ ├── DropObj.java │ │ │ │ ├── DeleteObj.java │ │ │ │ ├── Begin.java │ │ │ │ ├── InsertObj.java │ │ │ │ ├── SingleExpression.java │ │ │ │ ├── UpdateObj.java │ │ │ │ ├── SelectObj.java │ │ │ │ ├── OrderByExpression.java │ │ │ │ ├── Where.java │ │ │ │ └── Create.java │ │ │ └── Tokenizer.java │ │ ├── tbm │ │ │ ├── BeginRes.java │ │ │ ├── FieldCalRes.java │ │ │ ├── TableManager.java │ │ │ ├── Booter.java │ │ │ └── TableManagerImpl.java │ │ ├── utils │ │ │ ├── Panic.java │ │ │ ├── ParseStringRes.java │ │ │ ├── RandomUtil.java │ │ │ ├── Parser.java │ │ │ └── PrintUtil.java │ │ ├── dm │ │ │ ├── page │ │ │ │ ├── Page.java │ │ │ │ ├── PageImpl.java │ │ │ │ ├── PageOne.java │ │ │ │ └── PageX.java │ │ │ ├── pageIndex │ │ │ │ ├── PageInfo.java │ │ │ │ └── PageIndex.java │ │ │ ├── DataManager.java │ │ │ ├── dataItem │ │ │ │ ├── DataItem.java │ │ │ │ └── DataItemImpl.java │ │ │ ├── pageCache │ │ │ │ ├── PageCache.java │ │ │ │ └── PageCacheImpl.java │ │ │ ├── logger │ │ │ │ ├── Logger.java │ │ │ │ └── LoggerImpl.java │ │ │ ├── DataManagerImpl.java │ │ │ └── Recover.java │ │ ├── common │ │ │ ├── SubArray.java │ │ │ └── AbstractCache.java │ │ ├── vm │ │ │ ├── IsolationLevel.java │ │ │ ├── VersionManager.java │ │ │ ├── Transaction.java │ │ │ ├── Entry.java │ │ │ ├── Visibility.java │ │ │ ├── VersionManagerImpl.java │ │ │ └── LockTable.java │ │ ├── tm │ │ │ ├── TransactionManager.java │ │ │ └── TransactionManagerImpl.java │ │ ├── server │ │ │ ├── Executor.java │ │ │ └── Server.java │ │ ├── Launcher.java │ │ └── im │ │ │ └── UniqueIndex.java │ │ ├── transport │ │ ├── Package.java │ │ ├── Packager.java │ │ ├── Encoder.java │ │ └── Transporter.java │ │ ├── SimpleSqlDatabaseApplication.java │ │ ├── client │ │ ├── RoundTripper.java │ │ ├── Launcher.java │ │ ├── Client.java │ │ ├── l2.java │ │ ├── l3.java │ │ └── Shell.java │ │ ├── websocket │ │ ├── WebConfig.java │ │ ├── WebSocketConfig.java │ │ ├── UserSession.java │ │ ├── HttpSessionHandshakeInterceptor.java │ │ ├── UserManager.java │ │ └── TerminalWebSocketHandler.java │ │ └── common │ │ └── Error.java └── test │ └── java │ └── com │ └── dyx │ └── simpledb │ ├── ParserByteTest.java │ ├── tbm │ └── TableManagerTest.java │ ├── SimpleSqlDatabaseApplicationTests.java │ ├── vm │ ├── TransactionTimeoutTest.java │ └── LockTableTest.java │ ├── im │ └── UniqueIndexTest.java │ └── parser │ └── ParserTest.java ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── pom.xml └── mvnw.cmd /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | db: 3 | path: /www/wwwroot/db/ -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | db: 3 | path: D:/JavaCount/mydb/windows/ -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blockCloth/EasyDB/HEAD/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/Abort.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | public class Abort { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/Commit.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | public class Commit { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tbm/BeginRes.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tbm; 2 | 3 | public class BeginRes { 4 | public long xid; 5 | public byte[] result; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tbm/FieldCalRes.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tbm; 2 | 3 | public class FieldCalRes { 4 | public long left; 5 | public long right; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/Show.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | public class Show { 4 | public String tableName; 5 | public boolean isTable; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/DropObj.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class DropObj { 7 | public String tableName; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | devtools: 3 | livereload: 4 | enabled: true 5 | config: 6 | import: application-prod.yml 7 | application: 8 | name: simple-sql-database 9 | server: 10 | port: 8081 -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/utils/Panic.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.utils; 2 | 3 | public class Panic { 4 | public static void panic(Exception err) { 5 | err.printStackTrace(); 6 | // System.exit(1); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/DeleteObj.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class DeleteObj { 7 | public String tableName; 8 | public Where where; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/static/demo.5889606d.css: -------------------------------------------------------------------------------- 1 | html{background:linear-gradient(-90deg,#9c9a9a 10%,#4b4747 90%)}::-webkit-scrollbar{width:0}.fade{opacity:0;transition:all .6s!important;transform:translateY(200px)}.fade.in{opacity:1;transform:none} 2 | /*# sourceMappingURL=demo.5889606d.css.map */ -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/Begin.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import com.dyx.simpledb.backend.vm.IsolationLevel; 4 | import lombok.ToString; 5 | 6 | @ToString 7 | public class Begin { 8 | public IsolationLevel isolationLevel; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/InsertObj.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class InsertObj { 7 | public String tableName; 8 | public String[] fields; 9 | public String[] values; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/SingleExpression.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class SingleExpression { 7 | public String field; 8 | public String compareOp; 9 | public String value; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/page/Page.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.page; 2 | 3 | public interface Page { 4 | void lock(); 5 | void unlock(); 6 | void release(); 7 | void setDirty(boolean dirty); 8 | boolean isDirty(); 9 | int getPageNumber(); 10 | byte[] getData(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/utils/ParseStringRes.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.utils; 2 | 3 | public class ParseStringRes { 4 | public String str; 5 | public int next; 6 | 7 | public ParseStringRes(String str, int next) { 8 | this.str = str; 9 | this.next = next; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/UpdateObj.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class UpdateObj { 7 | public String tableName; 8 | public String[] fieldName; 9 | public String[] value; 10 | public Where where; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/pageIndex/PageInfo.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.pageIndex; 2 | 3 | public class PageInfo { 4 | public int pgno; 5 | public int freeSpace; 6 | 7 | public PageInfo(int pgno, int freeSpace) { 8 | this.pgno = pgno; 9 | this.freeSpace = freeSpace; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/SelectObj.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class SelectObj { 7 | public String tableName; 8 | public String[] fields; 9 | public Where where; 10 | public OrderByExpression orderByExpression; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/common/SubArray.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.common; 2 | 3 | public class SubArray { 4 | public byte[] raw; 5 | public int start; 6 | public int end; 7 | 8 | public SubArray(byte[] raw, int start, int end) { 9 | this.raw = raw; 10 | this.start = start; 11 | this.end = end; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/IsolationLevel.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | /** 4 | * @User Administrator 5 | * @CreateTime 2024/8/8 22:26 6 | * @className com.dyx.simpledb.backend.vm.IsolationLevel 7 | */ 8 | public enum IsolationLevel { 9 | READ_COMMITTED, 10 | READ_UNCOMMITTED, 11 | REPEATABLE_READ, 12 | SERIALIZABLE 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/utils/RandomUtil.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.utils; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.Random; 5 | 6 | public class RandomUtil { 7 | public static byte[] randomBytes(int length) { 8 | Random r = new SecureRandom(); 9 | byte[] buf = new byte[length]; 10 | r.nextBytes(buf); 11 | return buf; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/OrderByExpression.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | /** 4 | * @User Administrator 5 | * @CreateTime 2024/8/6 20:49 6 | * @className com.dyx.simpledb.backend.parser.statement.OrderByExpression 7 | */ 8 | public class OrderByExpression { 9 | public String[] fields; 10 | public Boolean[] order; 11 | 12 | public OrderByExpression() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/Where.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class Where { 7 | public SingleExpression singleExp1; 8 | public SingleExpression singleExp2; 9 | public String logicOp; 10 | 11 | public Where() {} 12 | 13 | public Where(SingleExpression exp) { 14 | this.singleExp1 = exp; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/transport/Package.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.transport; 2 | 3 | public class Package { 4 | byte[] data; 5 | Exception err; 6 | 7 | public Package(byte[] data, Exception err) { 8 | this.data = data; 9 | this.err = err; 10 | } 11 | 12 | public byte[] getData() { 13 | return data; 14 | } 15 | 16 | public Exception getErr() { 17 | return err; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/statement/Create.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser.statement; 2 | 3 | import lombok.ToString; 4 | 5 | @ToString 6 | public class Create { 7 | public String tableName; 8 | public String[] fieldName; 9 | public String[] fieldType; 10 | public String[] index; 11 | public String primaryKey; 12 | public String[] autoIncrement; 13 | public String[] notNull; 14 | public String[] unique; 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/ParserByteTest.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb; 2 | 3 | import com.dyx.simpledb.backend.utils.Parser; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | 8 | /** 9 | * @User Administrator 10 | * @CreateTime 2024/9/4 15:54 11 | * @className com.dyx.simpledb.ParserByteTest 12 | */ 13 | public class ParserByteTest { 14 | 15 | @Test 16 | public void constraintByteTest(){ 17 | byte[] bytes = Parser.constraintByte(true, false, true, true); 18 | System.out.println(Arrays.toString(bytes)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/SimpleSqlDatabaseApplication.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | @EnableScheduling 9 | @SpringBootApplication 10 | public class SimpleSqlDatabaseApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(SimpleSqlDatabaseApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/client/RoundTripper.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.client; 2 | 3 | import com.dyx.simpledb.transport.Package; 4 | import com.dyx.simpledb.transport.Packager; 5 | 6 | public class RoundTripper { 7 | private Packager packager; 8 | 9 | public RoundTripper(Packager packager) { 10 | this.packager = packager; 11 | } 12 | 13 | public Package roundTrip(Package pkg) throws Exception { 14 | packager.send(pkg); 15 | return packager.receive(); 16 | } 17 | 18 | public void close() throws Exception { 19 | packager.close(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/static/demo.5889606d.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["css ./node_modules/css-loader/dist/cjs.js??ref--11-1!./node_modules/postcss-loader/src??postcss!./demo/src/index.css"],"names":[],"mappings":"AAAA,KACA,0DACA,CACA,oBACA,OACA,CACA,MACA,SAAA,CACA,4BAAA,CACA,2BACA,CAEA,SACA,SAAA,CACA,cACA","file":"demo.5889606d.css","sourcesContent":["html {\n background: linear-gradient( -90deg, rgb(156, 154, 154) 10%, rgb(75, 71, 71) 90% );\n}\n::-webkit-scrollbar {\n width: 0px;\n}\n.fade {\n opacity: 0;\n transition: all 0.6s !important;\n transform: translateY(200px);\n}\n\n.fade.in {\n opacity: 1;\n transform: none;\n}\n"]} -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/tbm/TableManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.tbm; 2 | 3 | import com.dyx.simpledb.backend.server.Executor; 4 | import org.junit.Test; 5 | 6 | /** 7 | * @User Administrator 8 | * @CreateTime 2024/7/25 21:27 9 | * @className com.dyx.simpledb.tbm.TableManagerTest 10 | */ 11 | public class TableManagerTest { 12 | @Test 13 | public void createTable(){ 14 | String sql = "create table stu(id int,name varchar,index idx_name (name));"; 15 | 16 | // Executor executor = new Executor(); 17 | } 18 | 19 | @Test 20 | public void init(){ 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/client/Launcher.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.client; 2 | 3 | import java.io.IOException; 4 | import java.net.Socket; 5 | import java.net.UnknownHostException; 6 | 7 | import com.dyx.simpledb.transport.Encoder; 8 | import com.dyx.simpledb.transport.Packager; 9 | import com.dyx.simpledb.transport.Transporter; 10 | 11 | public class Launcher { 12 | public static void main(String[] args) throws UnknownHostException, IOException { 13 | Socket socket = new Socket("127.0.0.1", 9999); 14 | Encoder e = new Encoder(); 15 | Transporter t = new Transporter(socket); 16 | Packager packager = new Packager(t, e); 17 | 18 | Client client = new Client(packager); 19 | Shell shell = new Shell(client); 20 | shell.run(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/client/Client.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.client; 2 | 3 | import com.dyx.simpledb.transport.Package; 4 | import com.dyx.simpledb.transport.Packager; 5 | 6 | public class Client { 7 | private RoundTripper rt; 8 | 9 | public Client(Packager packager) { 10 | this.rt = new RoundTripper(packager); 11 | } 12 | 13 | public byte[] execute(byte[] stat) throws Exception { 14 | Package pkg = new Package(stat, null); 15 | Package resPkg = rt.roundTrip(pkg); 16 | if(resPkg.getErr() != null) { 17 | throw resPkg.getErr(); 18 | } 19 | return resPkg.getData(); 20 | } 21 | 22 | public void close() { 23 | try { 24 | rt.close(); 25 | } catch (Exception e) { 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/transport/Packager.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.transport; 2 | 3 | import com.dyx.simpledb.transport.Packager; 4 | 5 | public class Packager { 6 | private Transporter transpoter; 7 | private Encoder encoder; 8 | 9 | public Packager(Transporter transpoter, Encoder encoder) { 10 | this.transpoter = transpoter; 11 | this.encoder = encoder; 12 | } 13 | 14 | public void send(Package pkg) throws Exception { 15 | byte[] data = encoder.encode(pkg); 16 | transpoter.send(data); 17 | } 18 | 19 | public Package receive() throws Exception { 20 | byte[] data = transpoter.receive(); 21 | return encoder.decode(data); 22 | } 23 | 24 | public void close() throws Exception { 25 | transpoter.close(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/websocket/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.websocket; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | public class WebConfig implements WebMvcConfigurer { 10 | 11 | @Override 12 | public void addCorsMappings(CorsRegistry registry) { 13 | registry.addMapping("/**") 14 | .allowedOrigins("*") // 允许所有来源 15 | .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") 16 | .allowedHeaders("*") 17 | .allowCredentials(false); // 如果不需要Cookie支持,可以设置为false 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/VersionManager.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | import com.dyx.simpledb.backend.dm.DataManager; 4 | import com.dyx.simpledb.backend.tm.TransactionManager; 5 | 6 | public interface VersionManager { 7 | byte[] read(long xid, long uid) throws Exception; 8 | long insert(long xid, byte[] data) throws Exception; 9 | boolean delete(long xid, long uid) throws Exception; 10 | 11 | long begin(IsolationLevel isolationLevel); 12 | void commit(long xid) throws Exception; 13 | void abort(long xid); 14 | 15 | public static VersionManager newVersionManager(TransactionManager tm, DataManager dm) { 16 | return new VersionManagerImpl(tm, dm); 17 | } 18 | void physicalDelete(long xid, Long uid) throws Exception; 19 | 20 | Transaction getActiveTransaction(long xid); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/client/l2.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.client; 2 | 3 | import com.dyx.simpledb.transport.Encoder; 4 | import com.dyx.simpledb.transport.Packager; 5 | import com.dyx.simpledb.transport.Transporter; 6 | 7 | import java.io.IOException; 8 | import java.net.Socket; 9 | import java.net.UnknownHostException; 10 | 11 | /** 12 | * @User Administrator 13 | * @CreateTime 2024/8/14 22:47 14 | * @className com.dyx.simpledb.client.l2 15 | */ 16 | public class l2 { 17 | public static void main(String[] args) throws UnknownHostException, IOException { 18 | Socket socket = new Socket("127.0.0.1", 9999); 19 | Encoder e = new Encoder(); 20 | Transporter t = new Transporter(socket); 21 | Packager packager = new Packager(t, e); 22 | 23 | Client client = new Client(packager); 24 | Shell shell = new Shell(client); 25 | shell.run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/client/l3.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.client; 2 | 3 | import com.dyx.simpledb.transport.Encoder; 4 | import com.dyx.simpledb.transport.Packager; 5 | import com.dyx.simpledb.transport.Transporter; 6 | 7 | import java.io.IOException; 8 | import java.net.Socket; 9 | import java.net.UnknownHostException; 10 | 11 | /** 12 | * @User Administrator 13 | * @CreateTime 2024/8/14 22:48 14 | * @className com.dyx.simpledb.client.l3 15 | */ 16 | public class l3 { 17 | public static void main(String[] args) throws UnknownHostException, IOException { 18 | Socket socket = new Socket("127.0.0.1", 9999); 19 | Encoder e = new Encoder(); 20 | Transporter t = new Transporter(socket); 21 | Packager packager = new Packager(t, e); 22 | 23 | Client client = new Client(packager); 24 | Shell shell = new Shell(client); 25 | shell.run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/client/Shell.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.client; 2 | 3 | import java.util.Scanner; 4 | 5 | public class Shell { 6 | private Client client; 7 | 8 | public Shell(Client client) { 9 | this.client = client; 10 | } 11 | 12 | public void run() { 13 | Scanner sc = new Scanner(System.in); 14 | try { 15 | while(true) { 16 | System.out.print(":> "); 17 | String statStr = sc.nextLine(); 18 | if("exit".equals(statStr) || "quit".equals(statStr)) { 19 | break; 20 | } 21 | try { 22 | byte[] res = client.execute(statStr.getBytes()); 23 | System.out.println(new String(res)); 24 | } catch(Exception e) { 25 | System.out.println(e.getMessage()); 26 | } 27 | 28 | } 29 | } finally { 30 | sc.close(); 31 | client.close(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip 20 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/page/PageImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.page; 2 | 3 | import java.util.concurrent.locks.Lock; 4 | import java.util.concurrent.locks.ReentrantLock; 5 | 6 | import com.dyx.simpledb.backend.dm.pageCache.PageCache; 7 | 8 | public class PageImpl implements Page { 9 | private int pageNumber; 10 | private byte[] data; 11 | private boolean dirty; 12 | private Lock lock; 13 | 14 | private PageCache pc; 15 | 16 | public PageImpl(int pageNumber, byte[] data, PageCache pc) { 17 | this.pageNumber = pageNumber; 18 | this.data = data; 19 | this.pc = pc; 20 | lock = new ReentrantLock(); 21 | } 22 | 23 | public void lock() { 24 | lock.lock(); 25 | } 26 | 27 | public void unlock() { 28 | lock.unlock(); 29 | } 30 | 31 | public void release() { 32 | pc.release(this); 33 | } 34 | 35 | public void setDirty(boolean dirty) { 36 | this.dirty = dirty; 37 | } 38 | 39 | public boolean isDirty() { 40 | return dirty; 41 | } 42 | 43 | public int getPageNumber() { 44 | return pageNumber; 45 | } 46 | 47 | public byte[] getData() { 48 | return data; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/transport/Encoder.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.transport; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.google.common.primitives.Bytes; 6 | 7 | import com.dyx.simpledb.common.Error; 8 | 9 | public class Encoder { 10 | 11 | public byte[] encode(Package pkg) { 12 | if(pkg.getErr() != null) { 13 | Exception err = pkg.getErr(); 14 | String msg = "Intern server error!"; 15 | if(err.getMessage() != null) { 16 | msg = err.getMessage(); 17 | } 18 | return Bytes.concat(new byte[]{1}, msg.getBytes()); 19 | } else { 20 | return Bytes.concat(new byte[]{0}, pkg.getData()); 21 | } 22 | } 23 | 24 | public Package decode(byte[] data) throws Exception { 25 | if(data.length < 1) { 26 | throw Error.InvalidPkgDataException; 27 | } 28 | if(data[0] == 0) { 29 | return new Package(Arrays.copyOfRange(data, 1, data.length), null); 30 | } else if(data[0] == 1) { 31 | return new Package(null, new RuntimeException(new String(Arrays.copyOfRange(data, 1, data.length)))); 32 | } else { 33 | throw Error.InvalidPkgDataException; 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/SimpleSqlDatabaseApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb; 2 | 3 | import net.sf.jsqlparser.parser.CCJSqlParserUtil; 4 | import net.sf.jsqlparser.statement.Statement; 5 | import net.sf.jsqlparser.statement.create.table.ColumnDefinition; 6 | import net.sf.jsqlparser.statement.create.table.CreateTable; 7 | import org.junit.Test; 8 | 9 | 10 | public class SimpleSqlDatabaseApplicationTests { 11 | 12 | @Test 13 | public void testCreateTableParsing() { 14 | String createTableSQL = "CREATE TABLEs my_table (id INT PRIMARY KEY, name VARCHAR(100))"; 15 | 16 | try { 17 | Statement statement = CCJSqlParserUtil.parse(createTableSQL); 18 | if (statement instanceof CreateTable) { 19 | CreateTable createTable = (CreateTable) statement; 20 | System.out.println("Table Name: " + createTable.getTable().getName()); 21 | for (ColumnDefinition columnDefinition : createTable.getColumnDefinitions()) { 22 | System.out.println("Column: " + columnDefinition.getColumnName() + " " + columnDefinition.getColDataType()); 23 | } 24 | } 25 | } catch (Exception e) { 26 | e.printStackTrace(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/DataManager.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm; 2 | 3 | import com.dyx.simpledb.backend.dm.dataItem.DataItem; 4 | import com.dyx.simpledb.backend.dm.logger.Logger; 5 | import com.dyx.simpledb.backend.dm.page.PageOne; 6 | import com.dyx.simpledb.backend.dm.pageCache.PageCache; 7 | import com.dyx.simpledb.backend.tm.TransactionManager; 8 | 9 | public interface DataManager { 10 | DataItem read(long uid) throws Exception; 11 | long insert(long xid, byte[] data) throws Exception; 12 | void physicalDelete(Long uid) throws Exception; 13 | void close(); 14 | 15 | public static DataManager create(String path, long mem, TransactionManager tm) { 16 | PageCache pc = PageCache.create(path, mem); 17 | Logger lg = Logger.create(path); 18 | 19 | DataManagerImpl dm = new DataManagerImpl(pc, lg, tm); 20 | dm.initPageOne(); 21 | return dm; 22 | } 23 | 24 | public static DataManager open(String path, long mem, TransactionManager tm) { 25 | PageCache pc = PageCache.open(path, mem); 26 | Logger lg = Logger.open(path); 27 | DataManagerImpl dm = new DataManagerImpl(pc, lg, tm); 28 | if(!dm.loadCheckPageOne()) { 29 | Recover.recover(tm, lg, pc); 30 | } 31 | dm.fillPageIndex(); 32 | PageOne.setVcOpen(dm.pageOne); 33 | dm.pc.flushPage(dm.pageOne); 34 | 35 | return dm; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | logs/easydb.log 13 | 14 | 15 | 16 | 17 | logs/easydb-%d{yyyy-MM-dd}.%i.log 18 | 19 | 7 20 | 21 | 22 | 10MB 23 | 24 | 25 | 26 | 27 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tbm/TableManager.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tbm; 2 | 3 | import com.dyx.simpledb.backend.dm.DataManager; 4 | import com.dyx.simpledb.backend.parser.statement.*; 5 | import com.dyx.simpledb.backend.parser.statement.DeleteObj; 6 | import com.dyx.simpledb.backend.utils.Parser; 7 | import com.dyx.simpledb.backend.vm.VersionManager; 8 | 9 | public interface TableManager { 10 | BeginRes begin(Begin begin); 11 | byte[] commit(long xid) throws Exception; 12 | byte[] abort(long xid); 13 | 14 | byte[] show(long xid, Show stat); 15 | byte[] create(long xid, Create create) throws Exception; 16 | byte[] drop(long xid, DropObj stat) throws Exception; 17 | 18 | byte[] insert(long xid, InsertObj insertObj) throws Exception; 19 | byte[] read(long xid, SelectObj selectObj) throws Exception; 20 | byte[] update(long xid, UpdateObj updateObj) throws Exception; 21 | byte[] delete(long xid, DeleteObj deleteObj) throws Exception; 22 | 23 | // void close(); 24 | 25 | public static TableManager create(String path, VersionManager vm, DataManager dm) { 26 | Booter booter = Booter.create(path); 27 | booter.update(Parser.long2Byte(0)); 28 | return new TableManagerImpl(vm, dm, booter); 29 | } 30 | 31 | public static TableManager open(String path, VersionManager vm, DataManager dm) { 32 | Booter booter = Booter.open(path); 33 | return new TableManagerImpl(vm, dm, booter); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/page/PageOne.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.page; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.dyx.simpledb.backend.dm.pageCache.PageCache; 6 | import com.dyx.simpledb.backend.utils.RandomUtil; 7 | 8 | /** 9 | * 特殊管理第一页 10 | * ValidCheck 11 | * db启动时给100~107字节处填入一个随机字节,db关闭时将其拷贝到108~115字节 12 | * 用于判断上一次数据库是否正常关闭 13 | */ 14 | public class PageOne { 15 | private static final int OF_VC = 100; 16 | private static final int LEN_VC = 8; 17 | 18 | public static byte[] InitRaw() { 19 | byte[] raw = new byte[PageCache.PAGE_SIZE]; 20 | setVcOpen(raw); 21 | return raw; 22 | } 23 | 24 | public static void setVcOpen(Page pg) { 25 | pg.setDirty(true); 26 | setVcOpen(pg.getData()); 27 | } 28 | 29 | private static void setVcOpen(byte[] raw) { 30 | System.arraycopy(RandomUtil.randomBytes(LEN_VC), 0, raw, OF_VC, LEN_VC); 31 | } 32 | 33 | public static void setVcClose(Page pg) { 34 | pg.setDirty(true); 35 | setVcClose(pg.getData()); 36 | } 37 | 38 | private static void setVcClose(byte[] raw) { 39 | System.arraycopy(raw, OF_VC, raw, OF_VC+LEN_VC, LEN_VC); 40 | } 41 | 42 | public static boolean checkVc(Page pg) { 43 | return checkVc(pg.getData()); 44 | } 45 | 46 | private static boolean checkVc(byte[] raw) { 47 | return Arrays.equals(Arrays.copyOfRange(raw, OF_VC, OF_VC+LEN_VC), Arrays.copyOfRange(raw, OF_VC+LEN_VC, OF_VC+2*LEN_VC)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/static/runtime.b0a62080.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c[] lists; 17 | 18 | @SuppressWarnings("unchecked") 19 | public PageIndex() { 20 | lock = new ReentrantLock(); 21 | lists = new List[INTERVALS_NO+1]; 22 | for (int i = 0; i < INTERVALS_NO+1; i ++) { 23 | lists[i] = new ArrayList<>(); 24 | } 25 | } 26 | 27 | public void add(int pgno, int freeSpace) { 28 | lock.lock(); 29 | try { 30 | int number = freeSpace / THRESHOLD; 31 | lists[number].add(new PageInfo(pgno, freeSpace)); 32 | } finally { 33 | lock.unlock(); 34 | } 35 | } 36 | 37 | public PageInfo select(int spaceSize) { 38 | lock.lock(); 39 | try { 40 | int number = spaceSize / THRESHOLD; 41 | if(number < INTERVALS_NO) number ++; 42 | while(number <= INTERVALS_NO) { 43 | if(lists[number].size() == 0) { 44 | number ++; 45 | continue; 46 | } 47 | return lists[number].remove(0); 48 | } 49 | return null; 50 | } finally { 51 | lock.unlock(); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import com.dyx.simpledb.backend.tbm.Table; 9 | import com.dyx.simpledb.backend.tm.TransactionManagerImpl; 10 | 11 | // vm对一个事务的抽象 12 | public class Transaction { 13 | public long xid; 14 | public IsolationLevel isolationLevel; 15 | public Map snapshot; 16 | public Exception err; 17 | public boolean autoAborted; 18 | public long startTime; // 添加开始时间属性 19 | // 新增字段:记录事务中修改的表 20 | private Set modifiedTables = new HashSet<>(); 21 | 22 | // 添加修改表的方法 23 | public void addModifiedTable(Table table) { 24 | modifiedTables.add(table); 25 | } 26 | 27 | // 获取被修改的表 28 | public Set
getModifiedTables() { 29 | return modifiedTables; 30 | } 31 | 32 | public static Transaction newTransaction(long xid, IsolationLevel isolationLevel, Map active) { 33 | Transaction t = new Transaction(); 34 | t.xid = xid; 35 | t.isolationLevel = isolationLevel; 36 | t.startTime = System.currentTimeMillis(); 37 | if(isolationLevel != IsolationLevel.READ_COMMITTED && isolationLevel != IsolationLevel.READ_UNCOMMITTED) { 38 | t.snapshot = new HashMap<>(); 39 | for(Long x : active.keySet()) { 40 | t.snapshot.put(x, true); 41 | } 42 | } 43 | return t; 44 | } 45 | 46 | public boolean isInSnapshot(long xid) { 47 | if(xid == TransactionManagerImpl.SUPER_XID) { 48 | return false; 49 | } 50 | return snapshot.containsKey(xid); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/websocket/UserSession.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.websocket; 2 | 3 | import com.dyx.simpledb.backend.dm.DataManager; 4 | import com.dyx.simpledb.backend.server.Executor; 5 | import com.dyx.simpledb.backend.tbm.TableManager; 6 | import com.dyx.simpledb.backend.tm.TransactionManager; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | @Setter 16 | @Getter 17 | public class UserSession { 18 | private String userId; 19 | private long startTime; 20 | private long lastAccessedTime; 21 | private TableManager tableManager; 22 | private TransactionManager transactionManager; 23 | private DataManager dataManager; 24 | private Map executorMap; 25 | private final Set sessionIds = ConcurrentHashMap.newKeySet(); 26 | 27 | public UserSession(String userId, long startTime) { 28 | this.userId = userId; 29 | this.startTime = startTime; 30 | this.lastAccessedTime = startTime; // 初始化最后访问时间 31 | executorMap = new HashMap<>(); 32 | } 33 | 34 | public void updateLastAccessedTime() { 35 | this.lastAccessedTime = System.currentTimeMillis(); 36 | } 37 | 38 | public void close() { 39 | if (dataManager != null) { 40 | dataManager.close(); 41 | } 42 | if (transactionManager != null) { 43 | transactionManager.close(); 44 | } 45 | } 46 | 47 | public Executor getExecutor(String sessionId) { 48 | return executorMap.get(sessionId); 49 | } 50 | 51 | public Executor removeExecutor(String sessionId) { 52 | return executorMap.remove(sessionId); 53 | } 54 | 55 | public void setExecutor(String sessionId, Executor executor) { 56 | executorMap.put(sessionId,executor); 57 | } 58 | 59 | public void addSession(String sessionId) { 60 | sessionIds.add(sessionId); 61 | } 62 | 63 | public void removeSession(String sessionId) { 64 | sessionIds.remove(sessionId); 65 | } 66 | 67 | public boolean hasActiveSessions() { 68 | return !sessionIds.isEmpty(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/pageCache/PageCache.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.pageCache; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.channels.FileChannel; 7 | 8 | import com.dyx.simpledb.backend.dm.page.Page; 9 | import com.dyx.simpledb.backend.utils.Panic; 10 | import com.dyx.simpledb.common.Error; 11 | 12 | public interface PageCache { 13 | 14 | public static final int PAGE_SIZE = 1 << 13; 15 | 16 | int newPage(byte[] initData); 17 | Page getPage(int pgno) throws Exception; 18 | void close(); 19 | void release(Page page); 20 | 21 | void truncateByBgno(int maxPgno); 22 | int getPageNumber(); 23 | void flushPage(Page pg); 24 | 25 | public static PageCacheImpl create(String path, long memory) { 26 | File f = new File(path+PageCacheImpl.DB_SUFFIX); 27 | try { 28 | if(!f.createNewFile()) { 29 | Panic.panic(Error.FileExistsException); 30 | } 31 | } catch (Exception e) { 32 | Panic.panic(e); 33 | } 34 | if(!f.canRead() || !f.canWrite()) { 35 | Panic.panic(Error.FileCannotRWException); 36 | } 37 | 38 | FileChannel fc = null; 39 | RandomAccessFile raf = null; 40 | try { 41 | raf = new RandomAccessFile(f, "rw"); 42 | fc = raf.getChannel(); 43 | } catch (FileNotFoundException e) { 44 | Panic.panic(e); 45 | } 46 | return new PageCacheImpl(raf, fc, (int)memory/PAGE_SIZE); 47 | } 48 | 49 | public static PageCacheImpl open(String path, long memory) { 50 | File f = new File(path+PageCacheImpl.DB_SUFFIX); 51 | if(!f.exists()) { 52 | Panic.panic(Error.FileNotExistsException); 53 | } 54 | if(!f.canRead() || !f.canWrite()) { 55 | Panic.panic(Error.FileCannotRWException); 56 | } 57 | 58 | FileChannel fc = null; 59 | RandomAccessFile raf = null; 60 | try { 61 | raf = new RandomAccessFile(f, "rw"); 62 | fc = raf.getChannel(); 63 | } catch (FileNotFoundException e) { 64 | Panic.panic(e); 65 | } 66 | return new PageCacheImpl(raf, fc, (int)memory/PAGE_SIZE); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/page/PageX.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.page; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.dyx.simpledb.backend.dm.pageCache.PageCache; 6 | import com.dyx.simpledb.backend.utils.Parser; 7 | 8 | /** 9 | * PageX管理普通页 10 | * 普通页结构 11 | * [FreeSpaceOffset] [Data] 12 | * FreeSpaceOffset: 2字节 空闲位置开始偏移 13 | */ 14 | public class PageX { 15 | 16 | private static final short OF_FREE = 0; 17 | private static final short OF_DATA = 2; 18 | public static final int MAX_FREE_SPACE = PageCache.PAGE_SIZE - OF_DATA; 19 | 20 | public static byte[] initRaw() { 21 | byte[] raw = new byte[PageCache.PAGE_SIZE]; 22 | setFSO(raw, OF_DATA); 23 | return raw; 24 | } 25 | 26 | private static void setFSO(byte[] raw, short ofData) { 27 | System.arraycopy(Parser.short2Byte(ofData), 0, raw, OF_FREE, OF_DATA); 28 | } 29 | 30 | // 获取pg的FSO 31 | public static short getFSO(Page pg) { 32 | return getFSO(pg.getData()); 33 | } 34 | 35 | private static short getFSO(byte[] raw) { 36 | return Parser.parseShort(Arrays.copyOfRange(raw, 0, 2)); 37 | } 38 | 39 | // 将raw插入pg中,返回插入位置 40 | public static short insert(Page pg, byte[] raw) { 41 | pg.setDirty(true); 42 | short offset = getFSO(pg.getData()); 43 | System.arraycopy(raw, 0, pg.getData(), offset, raw.length); 44 | setFSO(pg.getData(), (short)(offset + raw.length)); 45 | return offset; 46 | } 47 | 48 | // 获取页面的空闲空间大小 49 | public static int getFreeSpace(Page pg) { 50 | return PageCache.PAGE_SIZE - (int)getFSO(pg.getData()); 51 | } 52 | 53 | // 将raw插入pg中的offset位置,并将pg的offset设置为较大的offset 54 | public static void recoverInsert(Page pg, byte[] raw, short offset) { 55 | pg.setDirty(true); 56 | System.arraycopy(raw, 0, pg.getData(), offset, raw.length); 57 | 58 | short rawFSO = getFSO(pg.getData()); 59 | if(rawFSO < offset + raw.length) { 60 | setFSO(pg.getData(), (short)(offset+raw.length)); 61 | } 62 | } 63 | 64 | // 将raw插入pg中的offset位置,不更新update 65 | public static void recoverUpdate(Page pg, byte[] raw, short offset) { 66 | pg.setDirty(true); 67 | System.arraycopy(raw, 0, pg.getData(), offset, raw.length); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/vm/TransactionTimeoutTest.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.vm; 2 | 3 | import com.dyx.simpledb.backend.dm.DataManager; 4 | import com.dyx.simpledb.backend.parser.statement.Create; 5 | import com.dyx.simpledb.backend.tm.TransactionManager; 6 | import com.dyx.simpledb.backend.tm.TransactionManagerImpl; 7 | import com.dyx.simpledb.backend.vm.IsolationLevel; 8 | import com.dyx.simpledb.backend.vm.VersionManager; 9 | import com.dyx.simpledb.backend.vm.VersionManagerImpl; 10 | import org.junit.Test; 11 | 12 | public class TransactionTimeoutTest { 13 | 14 | @Test 15 | public void timeoutTest() { 16 | // 初始化事务管理器和数据管理器 17 | TransactionManager tm = TransactionManager.open("D:\\JavaCount\\mydb\\windows\\127.0.0.1"); 18 | // 打开数据管理器,传入路径、内存大小和事务管理器 19 | DataManager dm = DataManager.open("D:\\JavaCount\\mydb\\windows\\127.0.0.1", (1 << 20) * 64, tm); 20 | // 创建版本管理器,传入事务管理器和数据管理器 21 | VersionManager vm = new VersionManagerImpl(tm, dm); 22 | 23 | // 开启一个长时间运行的事务 24 | long xid1 = vm.begin(IsolationLevel.READ_COMMITTED); 25 | 26 | // 在另一个线程中执行此事务,模拟长时间操作 27 | new Thread(() -> { 28 | try { 29 | // 模拟长时间操作 30 | System.out.println("Transaction " + xid1 + " started and sleeping..."); 31 | Thread.sleep(35000); // 超过30秒的超时时间 32 | System.out.println("Transaction " + xid1 + " completed."); 33 | } catch (Exception e) { 34 | e.printStackTrace(); 35 | } 36 | }).start(); 37 | 38 | // 在主线程中启动一个短时间事务以测试超时效果 39 | long xid2 = vm.begin(IsolationLevel.READ_COMMITTED); 40 | 41 | try { 42 | // 执行一些操作 43 | System.out.println("Transaction " + xid2 + " started."); 44 | vm.insert(xid2, "insert into user(id) values (10)".getBytes()); 45 | vm.commit(xid2); 46 | System.out.println("Transaction " + xid2 + " committed."); 47 | 48 | // 等待足够的时间让第一个事务超时 49 | Thread.sleep(40000); 50 | } catch (Exception e) { 51 | e.printStackTrace(); 52 | } 53 | 54 | // 检查第一个事务的状态 55 | if (tm.isAborted(xid1)) { 56 | System.out.println("Transaction " + xid1 + " was rolled back due to timeout."); 57 | } else { 58 | System.out.println("Transaction " + xid1 + " completed without timeout."); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/logger/Logger.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.logger; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.FileChannel; 9 | 10 | import com.dyx.simpledb.backend.utils.Panic; 11 | import com.dyx.simpledb.backend.utils.Parser; 12 | import com.dyx.simpledb.common.Error; 13 | 14 | public interface Logger { 15 | void log(byte[] data); 16 | void truncate(long x) throws Exception; 17 | byte[] next(); 18 | void rewind(); 19 | void close(); 20 | 21 | public static Logger create(String path) { 22 | File f = new File(path+LoggerImpl.LOG_SUFFIX); 23 | try { 24 | if(!f.createNewFile()) { 25 | Panic.panic(Error.FileExistsException); 26 | } 27 | } catch (Exception e) { 28 | Panic.panic(e); 29 | } 30 | if(!f.canRead() || !f.canWrite()) { 31 | Panic.panic(Error.FileCannotRWException); 32 | } 33 | 34 | FileChannel fc = null; 35 | RandomAccessFile raf = null; 36 | try { 37 | raf = new RandomAccessFile(f, "rw"); 38 | fc = raf.getChannel(); 39 | } catch (FileNotFoundException e) { 40 | Panic.panic(e); 41 | } 42 | 43 | ByteBuffer buf = ByteBuffer.wrap(Parser.int2Byte(0)); 44 | try { 45 | fc.position(0); 46 | fc.write(buf); 47 | fc.force(false); 48 | } catch (IOException e) { 49 | Panic.panic(e); 50 | } 51 | 52 | return new LoggerImpl(raf, fc, 0); 53 | } 54 | 55 | public static Logger open(String path) { 56 | File f = new File(path+LoggerImpl.LOG_SUFFIX); 57 | if(!f.exists()) { 58 | Panic.panic(Error.FileNotExistsException); 59 | } 60 | if(!f.canRead() || !f.canWrite()) { 61 | Panic.panic(Error.FileCannotRWException); 62 | } 63 | 64 | FileChannel fc = null; 65 | RandomAccessFile raf = null; 66 | try { 67 | raf = new RandomAccessFile(f, "rw"); 68 | fc = raf.getChannel(); 69 | } catch (FileNotFoundException e) { 70 | Panic.panic(e); 71 | } 72 | 73 | LoggerImpl lg = new LoggerImpl(raf, fc); 74 | lg.init(); 75 | 76 | return lg; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tm/TransactionManager.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tm; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.FileChannel; 9 | 10 | import com.dyx.simpledb.backend.utils.Panic; 11 | import com.dyx.simpledb.common.Error; 12 | 13 | public interface TransactionManager { 14 | long begin(); 15 | void commit(long xid); 16 | void abort(long xid); 17 | boolean isActive(long xid); 18 | boolean isCommitted(long xid); 19 | boolean isAborted(long xid); 20 | void close(); 21 | 22 | public static TransactionManagerImpl create(String path) { 23 | File f = new File(path+TransactionManagerImpl.XID_SUFFIX); 24 | try { 25 | if(!f.createNewFile()) { 26 | Panic.panic(Error.FileExistsException); 27 | } 28 | } catch (Exception e) { 29 | Panic.panic(e); 30 | } 31 | if(!f.canRead() || !f.canWrite()) { 32 | Panic.panic(Error.FileCannotRWException); 33 | } 34 | 35 | FileChannel fc = null; 36 | RandomAccessFile raf = null; 37 | try { 38 | raf = new RandomAccessFile(f, "rw"); 39 | fc = raf.getChannel(); 40 | } catch (FileNotFoundException e) { 41 | Panic.panic(e); 42 | } 43 | 44 | // 写空XID文件头 45 | ByteBuffer buf = ByteBuffer.wrap(new byte[TransactionManagerImpl.LEN_XID_HEADER_LENGTH]); 46 | try { 47 | fc.position(0); 48 | fc.write(buf); 49 | } catch (IOException e) { 50 | Panic.panic(e); 51 | } 52 | 53 | return new TransactionManagerImpl(raf, fc); 54 | } 55 | 56 | public static TransactionManagerImpl open(String path) { 57 | File f = new File(path+TransactionManagerImpl.XID_SUFFIX); 58 | if(!f.exists()) { 59 | Panic.panic(Error.FileNotExistsException); 60 | } 61 | if(!f.canRead() || !f.canWrite()) { 62 | Panic.panic(Error.FileCannotRWException); 63 | } 64 | 65 | FileChannel fc = null; 66 | RandomAccessFile raf = null; 67 | try { 68 | raf = new RandomAccessFile(f, "rw"); 69 | fc = raf.getChannel(); 70 | } catch (FileNotFoundException e) { 71 | Panic.panic(e); 72 | } 73 | 74 | return new TransactionManagerImpl(raf, fc); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/websocket/HttpSessionHandshakeInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.websocket; 2 | 3 | import org.springframework.http.server.ServletServerHttpRequest; 4 | import org.springframework.http.server.ServerHttpRequest; 5 | import org.springframework.http.server.ServerHttpResponse; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.WebSocketHandler; 8 | import org.springframework.web.socket.server.HandshakeInterceptor; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import java.util.Map; 12 | 13 | @Component 14 | public class HttpSessionHandshakeInterceptor implements HandshakeInterceptor { 15 | 16 | @Override 17 | public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { 18 | if (request instanceof ServletServerHttpRequest) { 19 | HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); 20 | String clientIp = getClientIp(servletRequest); 21 | attributes.put("clientIp", clientIp); 22 | } 23 | return true; 24 | } 25 | 26 | @Override 27 | public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { 28 | } 29 | 30 | private String getClientIp(HttpServletRequest request) { 31 | String clientIp = request.getHeader("X-Forwarded-For"); 32 | if (clientIp != null && !clientIp.isEmpty() && !"unknown".equalsIgnoreCase(clientIp)) { 33 | // 多重代理情况下,第一个IP是真实客户端IP 34 | clientIp = clientIp.split(",")[0]; 35 | } else { 36 | clientIp = request.getHeader("Proxy-Client-IP"); 37 | if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) { 38 | clientIp = request.getHeader("WL-Proxy-Client-IP"); 39 | } 40 | if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) { 41 | clientIp = request.getHeader("HTTP_CLIENT_IP"); 42 | } 43 | if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) { 44 | clientIp = request.getHeader("HTTP_X_FORWARDED_FOR"); 45 | } 46 | if (clientIp == null || clientIp.isEmpty() || "unknown".equalsIgnoreCase(clientIp)) { 47 | clientIp = request.getRemoteAddr(); 48 | } 49 | } 50 | 51 | if ("0:0:0:0:0:0:0:1".equals(clientIp)) { 52 | clientIp = "127.0.0.1"; 53 | } 54 | 55 | return clientIp; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyDB 2 | 3 |
4 | EasyDB Logo 5 |
6 | 7 | **轻量级、高性能的自定义数据库解决方案** 8 | 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 10 | [![Version](https://img.shields.io/badge/version-1.0.0-brightgreen.svg)](https://github.com/blockCloth/EasyDB) 11 | 12 | --- 13 | 14 | ## 简介 15 | 16 | **EasyDB** 是一个使用 Java 实现的轻量级数据库,灵感来源于 MySQL、PostgreSQL 和 SQLite。项目参考了 MYDB 的设计,专注于提供高效的数据库解决方案,支持数据存储、事务管理、多版本并发控制(MVCC)和索引管理等核心功能,同时集成了日志管理、事务隔离与死锁检测等高级特性。 17 | 18 | --- 19 | 20 | ## 主要特性 21 | 22 | ### 🛠️ 核心功能 23 | 24 | - **可靠性**:采用 MySQL、PostgreSQL 和 SQLite 的部分原理,确保数据的安全性与一致性。 25 | - **两阶段锁协议(2PL)**:实现串行化调度,支持多种事务隔离级别。 26 | - **多版本并发控制(MVCC)**:优化并发操作,减少阻塞,提高系统性能。 27 | 28 | ### 🌐 WebSocket 实时通信 29 | 30 | - **独立的数据区**:每个用户拥有独立的数据区,确保数据安全性和用户操作的互不干扰。 31 | - **多页面管理**:优化多页面访问体验,提升用户操作的流畅度。 32 | 33 | ### 🔍 高效 SQL 解析 34 | 35 | - **JSQLParser 集成**:使用 JSQLParser 将 SQL 语句解析为抽象语法树 (AST),简化 SQL 查询的分析与修改。 36 | 37 | ### ⚙️ 数据管理与优化 38 | 39 | - **全表扫描与索引处理**:支持在字段未建立索引的情况下进行条件筛选操作。 40 | - **条件约束**:内置丰富的条件约束与主键索引功能,支持唯一性、非空性、自增性等多种约束条件。 41 | 42 | ### 🚦 事务控制与死锁检测 43 | 44 | - **事务隔离机制**:支持从读未提交到串行化的多种隔离级别。 45 | - **死锁检测**:通过超时检测功能防止系统资源长期占用,增强系统的可靠性。 46 | 47 | ### 📝 日志管理与故障恢复 48 | 49 | - **日志管理**:内置强大的日志管理机制,确保数据库操作的可追溯性和数据一致性保障。 50 | - **故障恢复**:支持故障恢复功能,增强系统的容错能力和数据安全性。 51 | 52 | --- 53 | 54 | ## 快速开始 55 | 56 | ### 1. 克隆项目 57 | 58 | ```bash 59 | git clone https://github.com/blockCloth/EasyDB.git 60 | cd EasyDB 61 | ``` 62 | 63 | ### 2. 配置环境 64 | 65 | 请确保你已经安装了以下工具: 66 | 67 | - **JDK 8+**:Java 运行环境。 68 | - **Maven**:用于管理项目依赖。 69 | 70 | ### 3. 启动项目 71 | 72 | ```bash 73 | 启动SimpleSqlDatabaseApplication.java类即可 74 | ``` 75 | 76 | 项目启动成功后,可以通过访问 `http://localhost:8081/index.html` 进行体验。 77 | 78 | --- 79 | 80 | ## 使用指南 81 | 82 | 欲了解如何使用 EasyDB 的详细信息,请查阅以下文档: 83 | 84 | - [项目体验](http://db.blockcloth.cn/) 85 | - [使用指南](http://easydb.blockcloth.cn/document/) 86 | - [项目文档](http://easydb.blockcloth.cn/demo) 87 | 88 | --- 89 | 90 | ## 贡献 91 | 92 | 欢迎贡献代码和提交 Issue。如果你有任何问题或建议,请随时提交。 93 | 94 | 1. Fork 本仓库 95 | 2. 创建你的特性分支 (`git checkout -b feature/AmazingFeature`) 96 | 3. 提交你的修改 (`git commit -m 'Add some AmazingFeature'`) 97 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 98 | 5. 提交 Pull Request 99 | 100 | --- 101 | 102 | ## 许可证 103 | 104 | 本项目使用 [MIT 许可证](https://opensource.org/licenses/MIT) 开源,详情请查阅 `LICENSE` 文件。 105 | 106 | --- 107 | 108 | © 2024-至今 blockCloth 109 | 110 | --- 111 | 112 | ### 注意事项 113 | 114 | - 项目使用 Spring Boot 构建,确保项目配置正确。 115 | - 为了项目的顺利运行,请参阅文档中的详细配置步骤。 116 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/utils/Parser.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.utils; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.Arrays; 5 | 6 | import com.google.common.primitives.Bytes; 7 | 8 | public class Parser { 9 | 10 | public static byte[] short2Byte(short value) { 11 | return ByteBuffer.allocate(Short.SIZE / Byte.SIZE).putShort(value).array(); 12 | } 13 | 14 | public static short parseShort(byte[] buf) { 15 | ByteBuffer buffer = ByteBuffer.wrap(buf, 0, 2); 16 | return buffer.getShort(); 17 | } 18 | 19 | public static byte[] int2Byte(int value) { 20 | return ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).putInt(value).array(); 21 | } 22 | 23 | public static int parseInt(byte[] buf) { 24 | ByteBuffer buffer = ByteBuffer.wrap(buf, 0, 4); 25 | return buffer.getInt(); 26 | } 27 | 28 | public static float parseFloat(byte[] buf) { 29 | ByteBuffer buffer = ByteBuffer.wrap(buf, 0, 4); 30 | return buffer.getFloat(); 31 | } 32 | 33 | public static long parseLong(byte[] buf) { 34 | ByteBuffer buffer = ByteBuffer.wrap(buf, 0, 8); 35 | return buffer.getLong(); 36 | } 37 | 38 | public static double parseDouble(byte[] buf) { 39 | ByteBuffer buffer = ByteBuffer.wrap(buf, 0, 8); 40 | return buffer.getDouble(); 41 | } 42 | 43 | public static byte[] long2Byte(long value) { 44 | return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(value).array(); 45 | } 46 | 47 | public static byte[] double2Byte(double value) { 48 | return ByteBuffer.allocate(Double.SIZE / Byte.SIZE).putDouble(value).array(); 49 | } 50 | 51 | public static byte[] float2Byte(float value) { 52 | return ByteBuffer.allocate(Float.SIZE / Byte.SIZE).putFloat(value).array(); 53 | } 54 | 55 | public static ParseStringRes parseString(byte[] raw) { 56 | int length = parseInt(Arrays.copyOf(raw, 4)); 57 | String str = new String(Arrays.copyOfRange(raw, 4, 4+length)); 58 | return new ParseStringRes(str, length+4); 59 | } 60 | 61 | public static byte[] string2Byte(String str) { 62 | byte[] l = int2Byte(str.length()); 63 | return Bytes.concat(l, str.getBytes()); 64 | } 65 | 66 | public static long str2Uid(String key) { 67 | long seed = 13331; 68 | long res = 0; 69 | for(byte b : key.getBytes()) { 70 | res = res * seed + (long)b; 71 | } 72 | return res; 73 | } 74 | 75 | public static byte[] constraintByte(boolean isPrimaryKey, boolean isAutoIncrement, boolean isNotNull, boolean isUnique) { 76 | return Bytes.concat( 77 | new byte[] {isPrimaryKey ? (byte) 1 : (byte) 0}, 78 | new byte[] {isAutoIncrement ? (byte) 1 : (byte) 0}, 79 | new byte[] {isNotNull ? (byte) 1 : (byte) 0}, 80 | new byte[] {isUnique ? (byte) 1 : (byte) 0}); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tbm/Booter.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tbm; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.StandardCopyOption; 8 | 9 | import com.dyx.simpledb.backend.utils.Panic; 10 | import com.dyx.simpledb.common.Error; 11 | 12 | // 记录第一个表的uid 13 | public class Booter { 14 | public static final String BOOTER_SUFFIX = ".bt"; 15 | public static final String BOOTER_TMP_SUFFIX = ".bt_tmp"; 16 | 17 | String path; 18 | File file; 19 | 20 | public static Booter create(String path) { 21 | removeBadTmp(path); 22 | File f = new File(path+BOOTER_SUFFIX); 23 | try { 24 | if(!f.createNewFile()) { 25 | Panic.panic(Error.FileExistsException); 26 | } 27 | } catch (Exception e) { 28 | Panic.panic(e); 29 | } 30 | if(!f.canRead() || !f.canWrite()) { 31 | Panic.panic(Error.FileCannotRWException); 32 | } 33 | return new Booter(path, f); 34 | } 35 | 36 | public static Booter open(String path) { 37 | removeBadTmp(path); 38 | File f = new File(path+BOOTER_SUFFIX); 39 | if(!f.exists()) { 40 | Panic.panic(Error.FileNotExistsException); 41 | } 42 | if(!f.canRead() || !f.canWrite()) { 43 | Panic.panic(Error.FileCannotRWException); 44 | } 45 | return new Booter(path, f); 46 | } 47 | 48 | private static void removeBadTmp(String path) { 49 | new File(path+BOOTER_TMP_SUFFIX).delete(); 50 | } 51 | 52 | private Booter(String path, File file) { 53 | this.path = path; 54 | this.file = file; 55 | } 56 | 57 | public byte[] load() { 58 | byte[] buf = null; 59 | try { 60 | buf = Files.readAllBytes(file.toPath()); 61 | } catch (IOException e) { 62 | Panic.panic(e); 63 | } 64 | return buf; 65 | } 66 | 67 | public void update(byte[] data) { 68 | File tmp = new File(path + BOOTER_TMP_SUFFIX); 69 | try { 70 | tmp.createNewFile(); 71 | } catch (Exception e) { 72 | Panic.panic(e); 73 | } 74 | if(!tmp.canRead() || !tmp.canWrite()) { 75 | Panic.panic(Error.FileCannotRWException); 76 | } 77 | try(FileOutputStream out = new FileOutputStream(tmp)) { 78 | out.write(data); 79 | out.flush(); 80 | } catch(IOException e) { 81 | Panic.panic(e); 82 | } 83 | try { 84 | Files.move(tmp.toPath(), new File(path+BOOTER_SUFFIX).toPath(), StandardCopyOption.REPLACE_EXISTING); 85 | } catch(IOException e) { 86 | Panic.panic(e); 87 | } 88 | file = new File(path+BOOTER_SUFFIX); 89 | if(!file.canRead() || !file.canWrite()) { 90 | Panic.panic(Error.FileCannotRWException); 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/dataItem/DataItemImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.dataItem; 2 | 3 | import java.util.concurrent.locks.Lock; 4 | import java.util.concurrent.locks.ReadWriteLock; 5 | import java.util.concurrent.locks.ReentrantReadWriteLock; 6 | 7 | import com.dyx.simpledb.backend.common.SubArray; 8 | import com.dyx.simpledb.backend.dm.DataManagerImpl; 9 | import com.dyx.simpledb.backend.dm.page.Page; 10 | 11 | /** 12 | * dataItem 结构如下: 13 | * [ValidFlag] [DataSize] [Data] 14 | * ValidFlag 1字节,0为合法,1为非法 15 | * DataSize 2字节,标识Data的长度 16 | */ 17 | public class DataItemImpl implements DataItem { 18 | 19 | public static final int OF_VALID = 0; 20 | public static final int OF_SIZE = 1; 21 | public static final int OF_DATA = 3; 22 | 23 | private SubArray raw; 24 | private byte[] oldRaw; 25 | private Lock rLock; 26 | private Lock wLock; 27 | private DataManagerImpl dm; 28 | private long uid; 29 | private Page pg; 30 | 31 | public DataItemImpl(SubArray raw, byte[] oldRaw, Page pg, long uid, DataManagerImpl dm) { 32 | this.raw = raw; 33 | this.oldRaw = oldRaw; 34 | ReadWriteLock lock = new ReentrantReadWriteLock(); 35 | rLock = lock.readLock(); 36 | wLock = lock.writeLock(); 37 | this.dm = dm; 38 | this.uid = uid; 39 | this.pg = pg; 40 | } 41 | 42 | public boolean isValid() { 43 | return raw.raw[raw.start+OF_VALID] == (byte)0; 44 | } 45 | 46 | @Override 47 | public SubArray data() { 48 | return new SubArray(raw.raw, raw.start+OF_DATA, raw.end); 49 | } 50 | 51 | @Override 52 | public void before() { 53 | wLock.lock(); 54 | pg.setDirty(true); 55 | System.arraycopy(raw.raw, raw.start, oldRaw, 0, oldRaw.length); 56 | } 57 | 58 | @Override 59 | public void unBefore() { 60 | System.arraycopy(oldRaw, 0, raw.raw, raw.start, oldRaw.length); 61 | wLock.unlock(); 62 | } 63 | 64 | @Override 65 | public void after(long xid) { 66 | dm.logDataItem(xid, this); 67 | wLock.unlock(); 68 | } 69 | 70 | @Override 71 | public void release() { 72 | dm.releaseDataItem(this); 73 | } 74 | 75 | @Override 76 | public void lock() { 77 | wLock.lock(); 78 | } 79 | 80 | @Override 81 | public void unlock() { 82 | wLock.unlock(); 83 | } 84 | 85 | @Override 86 | public void rLock() { 87 | rLock.lock(); 88 | } 89 | 90 | @Override 91 | public void rUnLock() { 92 | rLock.unlock(); 93 | } 94 | 95 | @Override 96 | public Page page() { 97 | return pg; 98 | } 99 | 100 | @Override 101 | public long getUid() { 102 | return uid; 103 | } 104 | 105 | @Override 106 | public byte[] getOldRaw() { 107 | return oldRaw; 108 | } 109 | 110 | @Override 111 | public SubArray getRaw() { 112 | return raw; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/Entry.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.google.common.primitives.Bytes; 6 | 7 | import com.dyx.simpledb.backend.common.SubArray; 8 | import com.dyx.simpledb.backend.dm.dataItem.DataItem; 9 | import com.dyx.simpledb.backend.utils.Parser; 10 | 11 | /** 12 | * VM向上层抽象出entry 13 | * entry结构: 14 | * [XMIN] [XMAX] [data] 15 | */ 16 | public class Entry { 17 | 18 | private static final int OF_XMIN = 0; 19 | private static final int OF_XMAX = OF_XMIN+8; 20 | private static final int OF_DATA = OF_XMAX+8; 21 | 22 | private long uid; 23 | private DataItem dataItem; 24 | private VersionManager vm; 25 | 26 | public static Entry newEntry(VersionManager vm, DataItem dataItem, long uid) { 27 | if (dataItem == null) { 28 | return null; 29 | } 30 | Entry entry = new Entry(); 31 | entry.uid = uid; 32 | entry.dataItem = dataItem; 33 | entry.vm = vm; 34 | return entry; 35 | } 36 | 37 | public static Entry loadEntry(VersionManager vm, long uid) throws Exception { 38 | DataItem di = ((VersionManagerImpl)vm).dm.read(uid); 39 | return newEntry(vm, di, uid); 40 | } 41 | 42 | public static byte[] wrapEntryRaw(long xid, byte[] data) { 43 | byte[] xmin = Parser.long2Byte(xid); 44 | byte[] xmax = new byte[8]; 45 | return Bytes.concat(xmin, xmax, data); 46 | } 47 | 48 | public void release() { 49 | ((VersionManagerImpl)vm).releaseEntry(this); 50 | } 51 | 52 | public void remove() { 53 | dataItem.release(); 54 | } 55 | 56 | // 以拷贝的形式返回内容 57 | public byte[] data() { 58 | dataItem.rLock(); 59 | try { 60 | SubArray sa = dataItem.data(); 61 | byte[] data = new byte[sa.end - sa.start - OF_DATA]; 62 | System.arraycopy(sa.raw, sa.start+OF_DATA, data, 0, data.length); 63 | return data; 64 | } finally { 65 | dataItem.rUnLock(); 66 | } 67 | } 68 | 69 | public long getXmin() { 70 | dataItem.rLock(); 71 | try { 72 | SubArray sa = dataItem.data(); 73 | return Parser.parseLong(Arrays.copyOfRange(sa.raw, sa.start+OF_XMIN, sa.start+OF_XMAX)); 74 | } finally { 75 | dataItem.rUnLock(); 76 | } 77 | } 78 | 79 | public long getXmax() { 80 | dataItem.rLock(); 81 | try { 82 | SubArray sa = dataItem.data(); 83 | return Parser.parseLong(Arrays.copyOfRange(sa.raw, sa.start+OF_XMAX, sa.start+OF_DATA)); 84 | } finally { 85 | dataItem.rUnLock(); 86 | } 87 | } 88 | 89 | public void setXmax(long xid) { 90 | dataItem.before(); 91 | try { 92 | SubArray sa = dataItem.data(); 93 | System.arraycopy(Parser.long2Byte(xid), 0, sa.raw, sa.start+OF_XMAX, 8); 94 | } finally { 95 | dataItem.after(xid); 96 | } 97 | } 98 | 99 | public long getUid() { 100 | return uid; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/server/Executor.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.server; 2 | 3 | import com.dyx.simpledb.backend.parser.Parser; 4 | import com.dyx.simpledb.backend.parser.statement.*; 5 | import com.dyx.simpledb.backend.parser.statement.DeleteObj; 6 | import com.dyx.simpledb.backend.tbm.BeginRes; 7 | import com.dyx.simpledb.backend.tbm.TableManager; 8 | import com.dyx.simpledb.common.Error; 9 | 10 | public class Executor { 11 | private long xid; 12 | TableManager tbm; 13 | 14 | public Executor(TableManager tbm) { 15 | this.tbm = tbm; 16 | this.xid = 0; 17 | } 18 | 19 | public void close() { 20 | if(xid != 0) { 21 | System.out.println("Abnormal Abort: " + xid); 22 | tbm.abort(xid); 23 | } 24 | } 25 | 26 | public byte[] execute(byte[] sql) throws Exception { 27 | System.out.println("Execute: " + new String(sql)); 28 | Object stat = Parser.Parse(sql); 29 | if(Begin.class.isInstance(stat)) { 30 | if(xid != 0) { 31 | throw Error.NestedTransactionException; 32 | } 33 | BeginRes r = tbm.begin((Begin)stat); 34 | xid = r.xid; 35 | return r.result; 36 | } else if(Commit.class.isInstance(stat)) { 37 | if(xid == 0) { 38 | throw Error.NoTransactionException; 39 | } 40 | byte[] res = tbm.commit(xid); 41 | xid = 0; 42 | return res; 43 | } else if(Abort.class.isInstance(stat)) { 44 | if(xid == 0) { 45 | throw Error.NoTransactionException; 46 | } 47 | byte[] res = tbm.abort(xid); 48 | xid = 0; 49 | return res; 50 | } else { 51 | return execute2(stat); 52 | } 53 | } 54 | 55 | private byte[] execute2(Object stat) throws Exception { 56 | boolean tmpTransaction = false; 57 | Exception e = null; 58 | if(xid == 0) { 59 | tmpTransaction = true; 60 | BeginRes r = tbm.begin(new Begin()); 61 | xid = r.xid; 62 | } 63 | try { 64 | byte[] res = null; 65 | if(Show.class.isInstance(stat)) { 66 | res = tbm.show(xid,(Show)stat); 67 | } else if(Create.class.isInstance(stat)) { 68 | res = tbm.create(xid, (Create)stat); 69 | } else if(SelectObj.class.isInstance(stat)) { 70 | res = tbm.read(xid, (SelectObj)stat); 71 | } else if(InsertObj.class.isInstance(stat)) { 72 | res = tbm.insert(xid, (InsertObj)stat); 73 | } else if(DeleteObj.class.isInstance(stat)) { 74 | res = tbm.delete(xid, (DeleteObj)stat); 75 | } else if(UpdateObj.class.isInstance(stat)) { 76 | res = tbm.update(xid, (UpdateObj)stat); 77 | }else if(DropObj.class.isInstance(stat)) { 78 | res = tbm.drop(xid, (DropObj)stat); 79 | } 80 | return res; 81 | } catch(Exception e1) { 82 | e = e1; 83 | throw e; 84 | } finally { 85 | if(tmpTransaction) { 86 | if(e != null) { 87 | tbm.abort(xid); 88 | } else { 89 | tbm.commit(xid); 90 | } 91 | xid = 0; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/utils/PrintUtil.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.utils; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | public class PrintUtil { 8 | 9 | /** 10 | * 生成单列或多列表格并返回其字符串表示 11 | * 12 | * @param columnNames 列名数组 13 | * @param entries 数据列表,每个Map代表一行,key是列名,value是列值 14 | * @return 格式化的表格字符串 15 | */ 16 | public static String printTable(String[] columnNames, List> entries) { 17 | // 计算每列的最大宽度 18 | Map columnWidths = calculateColumnWidths(entries, columnNames); 19 | 20 | // 创建StringBuilder用于构建输出 21 | StringBuilder sb = new StringBuilder(); 22 | 23 | // 打印表头分隔符 24 | printSeparator(sb, columnWidths, columnNames); 25 | 26 | // 打印列名 27 | printColumnNames(sb, columnWidths, columnNames); 28 | 29 | // 打印表头分隔符 30 | printSeparator(sb, columnWidths, columnNames); 31 | 32 | // 打印数据行 33 | printData(sb, columnWidths, columnNames, entries); 34 | 35 | // 打印表尾分隔符 36 | printSeparator(sb, columnWidths, columnNames); 37 | 38 | // 返回最终结果字符串 39 | return sb.toString().endsWith("\n") 40 | ? sb.toString().substring(0, sb.toString().length() - 1) 41 | : sb.toString(); 42 | } 43 | 44 | private static Map calculateColumnWidths(List> entries, String[] selectedFields) { 45 | Map columnWidths = new HashMap<>(); 46 | for (String col : selectedFields) { 47 | int maxLength = col.length(); 48 | for (Map entry : entries) { 49 | String value = entry.get(col) != null ? entry.get(col).toString() : "NULL"; 50 | if (value.length() > maxLength) { 51 | maxLength = value.length(); 52 | } 53 | } 54 | columnWidths.put(col, maxLength); 55 | } 56 | return columnWidths; 57 | } 58 | 59 | private static void printSeparator(StringBuilder sb, Map columnWidths, String[] selectedFields) { 60 | sb.append("+"); 61 | for (String col : selectedFields) { 62 | int width = columnWidths.get(col); 63 | sb.append(repeat("-", width)).append("+"); 64 | } 65 | sb.append("\n"); 66 | } 67 | 68 | private static void printColumnNames(StringBuilder sb, Map columnWidths, String[] selectedFields) { 69 | sb.append("|"); 70 | for (String col : selectedFields) { 71 | int width = columnWidths.get(col); 72 | sb.append(String.format("%-" + width + "s|", col)); 73 | } 74 | sb.append("\n"); 75 | } 76 | 77 | private static void printData(StringBuilder sb, Map columnWidths, String[] selectedFields, List> entries) { 78 | for (Map entry : entries) { 79 | sb.append("|"); 80 | for (String col : selectedFields) { 81 | String value = entry.get(col) != null ? entry.get(col).toString() : "NULL"; 82 | int width = columnWidths.get(col); 83 | sb.append(String.format("%-" + width + "s|", value)); 84 | } 85 | sb.append("\n"); 86 | } 87 | } 88 | 89 | private static String repeat(String str, int times) { 90 | StringBuilder sb = new StringBuilder(); 91 | for (int i = 0; i < times; i++) { 92 | sb.append(str); 93 | } 94 | return sb.toString(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/websocket/UserManager.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.websocket; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.scheduling.annotation.Scheduled; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.*; 10 | import java.nio.file.attribute.BasicFileAttributes; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | @Component 15 | public class UserManager { 16 | private static final int MAX_USERS = 20; 17 | private static final int SESSION_EXPIRY_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds 18 | private static final int MAX_SESSION_DURATION = 2 * 60 * 60 * 1000; // 2 hours in milliseconds 19 | 20 | private ConcurrentHashMap activeUsers = new ConcurrentHashMap<>(); 21 | private AtomicInteger userCount = new AtomicInteger(0); 22 | 23 | @Value("${custom.db.path}") 24 | private String dbPath; 25 | 26 | public boolean canInit(String userId) { 27 | if (userCount.get() >= MAX_USERS) { 28 | return false; 29 | } 30 | activeUsers.put(userId, new UserSession(userId, System.currentTimeMillis())); 31 | userCount.incrementAndGet(); 32 | return true; 33 | } 34 | 35 | public UserSession getUserSession(String userId) { 36 | return activeUsers.get(userId); 37 | } 38 | 39 | public void addUserSession(String userId, UserSession userSession) { 40 | activeUsers.put(userId, userSession); 41 | userCount.incrementAndGet(); 42 | } 43 | 44 | public void removeUserSession(String userId) { 45 | // 先关闭数据库文件 46 | UserSession session = activeUsers.get(userId); 47 | if (session != null) { 48 | session.close(); 49 | } 50 | activeUsers.remove(userId); 51 | userCount.decrementAndGet(); 52 | } 53 | 54 | @Scheduled(fixedRate = 60000) // 每分钟执行一次 55 | public void checkSessions() { 56 | long currentTime = System.currentTimeMillis(); 57 | activeUsers.forEach((userId, session) -> { 58 | if (currentTime - session.getLastAccessedTime() >= SESSION_EXPIRY_CHECK_INTERVAL || 59 | currentTime - session.getStartTime() >= MAX_SESSION_DURATION) { 60 | removeUserSession(userId); 61 | destroyDatabase(userId); 62 | } 63 | }); 64 | } 65 | 66 | public void destroyDatabase(String userId) { 67 | // 删除数据库目录和文件的逻辑 68 | String directoryPath = dbPath + File.separator + userId; 69 | Path directory = Paths.get(directoryPath); 70 | try { 71 | deleteDirectory(directory); 72 | } catch (IOException e) { 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | private void deleteDirectory(Path directory) throws IOException { 78 | Files.walkFileTree(directory, new SimpleFileVisitor() { 79 | @Override 80 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 81 | Files.delete(file); 82 | return FileVisitResult.CONTINUE; 83 | } 84 | 85 | @Override 86 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 87 | if (exc == null) { 88 | Files.delete(dir); 89 | return FileVisitResult.CONTINUE; 90 | } else { 91 | throw exc; 92 | } 93 | } 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/server/Server.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.server; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.net.ServerSocket; 6 | import java.net.Socket; 7 | import java.util.concurrent.ArrayBlockingQueue; 8 | import java.util.concurrent.ThreadPoolExecutor; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import com.dyx.simpledb.backend.tbm.TableManager; 12 | import com.dyx.simpledb.transport.Encoder; 13 | import com.dyx.simpledb.transport.Package; 14 | import com.dyx.simpledb.transport.Packager; 15 | import com.dyx.simpledb.transport.Transporter; 16 | 17 | public class Server { 18 | private int port; 19 | TableManager tbm; 20 | 21 | public Server(int port, TableManager tbm) { 22 | this.port = port; 23 | this.tbm = tbm; 24 | } 25 | 26 | public void start() { 27 | ServerSocket ss = null; 28 | try { 29 | ss = new ServerSocket(port); 30 | } catch (IOException e) { 31 | e.printStackTrace(); 32 | return; 33 | } 34 | System.out.println("Server listen to port: " + port); 35 | ThreadPoolExecutor tpe = new ThreadPoolExecutor(10, 20, 1L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy()); 36 | try { 37 | while(true) { 38 | Socket socket = ss.accept(); 39 | Runnable worker = new HandleSocket(socket, tbm); 40 | tpe.execute(worker); 41 | } 42 | } catch(IOException e) { 43 | e.printStackTrace(); 44 | } finally { 45 | try { 46 | ss.close(); 47 | } catch (IOException ignored) {} 48 | } 49 | } 50 | } 51 | 52 | class HandleSocket implements Runnable { 53 | private Socket socket; 54 | private TableManager tbm; 55 | 56 | public HandleSocket(Socket socket, TableManager tbm) { 57 | this.socket = socket; 58 | this.tbm = tbm; 59 | } 60 | 61 | @Override 62 | public void run() { 63 | InetSocketAddress address = (InetSocketAddress)socket.getRemoteSocketAddress(); 64 | System.out.println("Establish connection: " + address.getAddress().getHostAddress()+":"+address.getPort()); 65 | Packager packager = null; 66 | try { 67 | Transporter t = new Transporter(socket); 68 | Encoder e = new Encoder(); 69 | packager = new Packager(t, e); 70 | } catch(IOException e) { 71 | e.printStackTrace(); 72 | try { 73 | socket.close(); 74 | } catch (IOException e1) { 75 | e1.printStackTrace(); 76 | } 77 | return; 78 | } 79 | Executor exe = new Executor(tbm); 80 | while(true) { 81 | Package pkg = null; 82 | try { 83 | pkg = packager.receive(); 84 | } catch(Exception e) { 85 | break; 86 | } 87 | byte[] sql = pkg.getData(); 88 | byte[] result = null; 89 | Exception e = null; 90 | try { 91 | result = exe.execute(sql); 92 | } catch (Exception e1) { 93 | e = e1; 94 | e.printStackTrace(); 95 | } 96 | pkg = new Package(result, e); 97 | try { 98 | packager.send(pkg); 99 | } catch (Exception e1) { 100 | e1.printStackTrace(); 101 | break; 102 | } 103 | } 104 | exe.close(); 105 | try { 106 | packager.close(); 107 | } catch (Exception e) { 108 | e.printStackTrace(); 109 | } 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/Visibility.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | import com.dyx.simpledb.backend.tm.TransactionManager; 4 | 5 | public class Visibility { 6 | 7 | public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) { 8 | long xmax = e.getXmax(); 9 | 10 | if (t.isolationLevel == IsolationLevel.READ_UNCOMMITTED){ 11 | return false; 12 | }else if (t.isolationLevel == IsolationLevel.READ_COMMITTED){ 13 | return false; 14 | }else if (t.isolationLevel == IsolationLevel.REPEATABLE_READ){ 15 | return tm.isCommitted(xmax) && (xmax > t.xid || t.isInSnapshot(xmax)); 16 | }else if (t.isolationLevel == IsolationLevel.SERIALIZABLE){ 17 | return tm.isCommitted(xmax) && (xmax > t.xid || t.isInSnapshot(xmax)); 18 | }else { 19 | throw new IllegalArgumentException("Unknown isolation level: " + t.isolationLevel); 20 | } 21 | } 22 | 23 | public static boolean isVisible(TransactionManager tm, Transaction t, Entry e) { 24 | switch (t.isolationLevel) { 25 | case READ_UNCOMMITTED: 26 | return readUnCommitted(tm, t, e); 27 | case READ_COMMITTED: 28 | return readCommitted(tm, t, e); 29 | case REPEATABLE_READ: 30 | return repeatableRead(tm, t, e); 31 | case SERIALIZABLE: 32 | return serializable(tm, t, e); 33 | default: 34 | throw new IllegalArgumentException("Unknown isolation level: " + t.isolationLevel); 35 | } 36 | } 37 | 38 | // 读未提交,允许所有事务读取数据 39 | private static boolean readUnCommitted(TransactionManager tm, Transaction t, Entry e) { 40 | long xmax = e.getXmax(); 41 | 42 | // 检查数据是否被删除,如果未删除则可见 43 | return xmax == 0; 44 | } 45 | 46 | private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) { 47 | long xid = t.xid; 48 | long xmin = e.getXmin(); 49 | long xmax = e.getXmax(); 50 | if (xmin == xid && xmax == 0) return true; 51 | 52 | if (tm.isCommitted(xmin)) { 53 | if (xmax == 0) return true; 54 | if (xmax != xid) { 55 | if (!tm.isCommitted(xmax)) { 56 | return true; 57 | } 58 | } 59 | } 60 | return false; 61 | } 62 | 63 | private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) { 64 | long xid = t.xid; 65 | long xmin = e.getXmin(); 66 | long xmax = e.getXmax(); 67 | if (xmin == xid && xmax == 0) return true; 68 | 69 | if (tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) { 70 | if (xmax == 0) return true; 71 | if (xmax != xid) { 72 | if (!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) { 73 | return true; 74 | } 75 | } 76 | } 77 | return false; 78 | } 79 | 80 | private static boolean serializable(TransactionManager tm, Transaction t, Entry e) { 81 | long xid = t.xid; 82 | long xmin = e.getXmin(); 83 | long xmax = e.getXmax(); 84 | 85 | // 当前事务创建且尚未删除 86 | if (xmin == xid && xmax == 0) return true; 87 | 88 | // 由已提交事务创建且在当前事务之前提交 89 | if (tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) { 90 | // 尚未删除 91 | if (xmax == 0) return true; 92 | // 由其他事务删除,但该删除操作尚未提交,或在当前事务之后开始,或在当前事务开始时仍未提交 93 | if (xmax != xid) { 94 | if (!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) { 95 | return true; 96 | } 97 | } 98 | } 99 | return false; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | EasyDB 11 | 12 | 13 | 14 | 15 |
16 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/Launcher.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.commons.cli.CommandLineParser; 5 | import org.apache.commons.cli.DefaultParser; 6 | import org.apache.commons.cli.Options; 7 | import org.apache.commons.cli.ParseException; 8 | 9 | import com.dyx.simpledb.backend.dm.DataManager; 10 | import com.dyx.simpledb.backend.server.Server; 11 | import com.dyx.simpledb.backend.tbm.TableManager; 12 | import com.dyx.simpledb.backend.tm.TransactionManager; 13 | import com.dyx.simpledb.backend.utils.Panic; 14 | import com.dyx.simpledb.backend.vm.VersionManager; 15 | import com.dyx.simpledb.backend.vm.VersionManagerImpl; 16 | import com.dyx.simpledb.common.Error; 17 | 18 | public class Launcher { 19 | // 定义服务器监听的端口号 20 | public static final int port = 9999; 21 | // 定义默认的内存大小,这里是64MB,用于数据管理器 22 | public static final long DEFALUT_MEM = (1 << 20) * 64; 23 | // 定义一些内存单位,用于解析命令行参数中的内存大小 24 | public static final long KB = 1 << 10; // 1KB 25 | public static final long MB = 1 << 20; // 1MB 26 | public static final long GB = 1 << 30; // 1GB 27 | 28 | public static void main(String[] args) throws ParseException { 29 | Options options = new Options(); 30 | options.addOption("open", true, "-open D:/"); 31 | options.addOption("create", true, "-create DBPath"); 32 | options.addOption("mem", true, "-mem 64MB"); 33 | CommandLineParser parser = new DefaultParser(); 34 | CommandLine cmd = parser.parse(options, args); 35 | 36 | if (cmd.hasOption("open")) { 37 | openDB(cmd.getOptionValue("open"), parseMem(cmd.getOptionValue("mem"))); 38 | return; 39 | } 40 | if (cmd.hasOption("create")) { 41 | createDB(cmd.getOptionValue("create")); 42 | return; 43 | } 44 | System.out.println("Usage: launcher (open|create) DBPath"); 45 | } 46 | 47 | /** 48 | * 创建新的数据库 49 | * 50 | * @param path 数据库路径 51 | */ 52 | private static void createDB(String path) { 53 | // 创建事务管理器 54 | TransactionManager tm = TransactionManager.create(path); 55 | // 创建数据管理器 56 | DataManager dm = DataManager.create(path, DEFALUT_MEM, tm); 57 | // 创建版本管理器 58 | VersionManager vm = new VersionManagerImpl(tm, dm); 59 | // 创建表管理器 60 | TableManager.create(path, vm, dm); 61 | tm.close(); 62 | dm.close(); 63 | } 64 | 65 | /** 66 | * 启动已有的数据库 67 | */ 68 | private static void openDB(String path, long mem) { 69 | // 打开事务管理器 70 | TransactionManager tm = TransactionManager.open(path); 71 | // 打开数据管理器,传入路径、内存大小和事务管理器 72 | DataManager dm = DataManager.open(path, mem, tm); 73 | // 创建版本管理器,传入事务管理器和数据管理器 74 | VersionManager vm = new VersionManagerImpl(tm, dm); 75 | // 打开表管理器,传入路径、版本管理器和数据管理器 76 | TableManager tbm = TableManager.open(path, vm, dm); 77 | // 创建服务器对象,并启动服务器 78 | new Server(port, tbm).start(); 79 | } 80 | 81 | // 定义一个方法,用于解析命令行参数中的内存大小 82 | private static long parseMem(String memStr) { 83 | // 如果内存大小为空或者为空字符串,那么返回默认的内存大小 84 | if (memStr == null || "".equals(memStr)) { 85 | return DEFALUT_MEM; 86 | } 87 | // 如果内存大小的字符串长度小于2,那么抛出异常 88 | if (memStr.length() < 2) { 89 | Panic.panic(Error.InvalidMemException); 90 | } 91 | // 获取内存大小的单位,即字符串的后两个字符 92 | String unit = memStr.substring(memStr.length() - 2); 93 | // 获取内存大小的数值部分,即字符串的前部分,并转换为数字 94 | long memNum = Long.parseLong(memStr.substring(0, memStr.length() - 2)); 95 | // 根据内存单位,计算并返回最终的内存大小 96 | switch (unit) { 97 | case "KB": 98 | return memNum * KB; 99 | case "MB": 100 | return memNum * MB; 101 | case "GB": 102 | return memNum * GB; 103 | // 如果内存单位不是KB、MB或GB,那么抛出异常 104 | default: 105 | Panic.panic(Error.InvalidMemException); 106 | } 107 | // 如果没有匹配到任何情况,那么返回默认的内存大小 108 | return DEFALUT_MEM; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/common/AbstractCache.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.common; 2 | 3 | import java.util.HashMap; 4 | import java.util.Set; 5 | import java.util.concurrent.locks.Lock; 6 | import java.util.concurrent.locks.ReentrantLock; 7 | 8 | import com.dyx.simpledb.common.Error; 9 | 10 | /** 11 | * AbstractCache 实现了一个引用计数策略的缓存 12 | */ 13 | public abstract class AbstractCache { 14 | private HashMap cache; // 实际缓存的数据 15 | private HashMap references; // 元素的引用个数 16 | private HashMap getting; // 正在获取某资源的线程 17 | 18 | private int maxResource; // 缓存的最大缓存资源数 19 | private int count = 0; // 缓存中元素的个数 20 | private Lock lock; 21 | 22 | public AbstractCache(int maxResource) { 23 | this.maxResource = maxResource; 24 | cache = new HashMap<>(); 25 | references = new HashMap<>(); 26 | getting = new HashMap<>(); 27 | lock = new ReentrantLock(); 28 | } 29 | 30 | protected T get(long key) throws Exception { 31 | while(true) { 32 | lock.lock(); 33 | if(getting.containsKey(key)) { 34 | // 请求的资源正在被其他线程获取 35 | lock.unlock(); 36 | try { 37 | Thread.sleep(1); 38 | } catch (InterruptedException e) { 39 | e.printStackTrace(); 40 | continue; 41 | } 42 | continue; 43 | } 44 | 45 | if(cache.containsKey(key)) { 46 | // 资源在缓存中,直接返回 47 | T obj = cache.get(key); 48 | references.put(key, references.get(key) + 1); 49 | lock.unlock(); 50 | return obj; 51 | } 52 | 53 | // 尝试获取该资源 54 | if(maxResource > 0 && count == maxResource) { 55 | lock.unlock(); 56 | throw Error.CacheFullException; 57 | } 58 | count ++; 59 | getting.put(key, true); 60 | lock.unlock(); 61 | break; 62 | } 63 | 64 | T obj = null; 65 | try { 66 | obj = getForCache(key); 67 | } catch(Exception e) { 68 | lock.lock(); 69 | count --; 70 | getting.remove(key); 71 | lock.unlock(); 72 | throw e; 73 | } 74 | 75 | lock.lock(); 76 | getting.remove(key); 77 | cache.put(key, obj); 78 | references.put(key, 1); 79 | lock.unlock(); 80 | 81 | return obj; 82 | } 83 | 84 | /** 85 | * 强行释放一个缓存 86 | */ 87 | protected void release(long key) { 88 | lock.lock(); 89 | try { 90 | // 检查 references 中是否包含该 key 91 | if (!references.containsKey(key)) { 92 | // 如果 references 中不包含该 key,直接返回,不进行释放 93 | return; 94 | } 95 | 96 | int ref = references.get(key)-1; 97 | if(ref == 0) { 98 | T obj = cache.get(key); 99 | releaseForCache(obj); 100 | references.remove(key); 101 | cache.remove(key); 102 | count --; 103 | } else { 104 | references.put(key, ref); 105 | } 106 | } finally { 107 | lock.unlock(); 108 | } 109 | } 110 | 111 | /** 112 | * 关闭缓存,写回所有资源 113 | */ 114 | protected void close() { 115 | lock.lock(); 116 | try { 117 | Set keys = cache.keySet(); 118 | for (long key : keys) { 119 | T obj = cache.get(key); 120 | releaseForCache(obj); 121 | references.remove(key); 122 | cache.remove(key); 123 | } 124 | } finally { 125 | lock.unlock(); 126 | } 127 | } 128 | 129 | 130 | /** 131 | * 当资源不在缓存时的获取行为 132 | */ 133 | protected abstract T getForCache(long key) throws Exception; 134 | /** 135 | * 当资源被驱逐时的写回行为 136 | */ 137 | protected abstract void releaseForCache(T obj); 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/pageCache/PageCacheImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.pageCache; 2 | 3 | import java.io.IOException; 4 | import java.io.RandomAccessFile; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.FileChannel; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | import java.util.concurrent.locks.Lock; 9 | import java.util.concurrent.locks.ReentrantLock; 10 | 11 | import com.dyx.simpledb.backend.common.AbstractCache; 12 | import com.dyx.simpledb.backend.dm.page.Page; 13 | import com.dyx.simpledb.backend.dm.page.PageImpl; 14 | import com.dyx.simpledb.backend.utils.Panic; 15 | import com.dyx.simpledb.common.Error; 16 | 17 | public class PageCacheImpl extends AbstractCache implements PageCache { 18 | 19 | private static final int MEM_MIN_LIM = 10; 20 | public static final String DB_SUFFIX = ".db"; 21 | 22 | private RandomAccessFile file; 23 | private FileChannel fc; 24 | private Lock fileLock; 25 | 26 | private AtomicInteger pageNumbers; 27 | 28 | PageCacheImpl(RandomAccessFile file, FileChannel fileChannel, int maxResource) { 29 | super(maxResource); 30 | if(maxResource < MEM_MIN_LIM) { 31 | Panic.panic(Error.MemTooSmallException); 32 | } 33 | long length = 0; 34 | try { 35 | length = file.length(); 36 | } catch (IOException e) { 37 | Panic.panic(e); 38 | } 39 | this.file = file; 40 | this.fc = fileChannel; 41 | this.fileLock = new ReentrantLock(); 42 | this.pageNumbers = new AtomicInteger((int)length / PAGE_SIZE); 43 | } 44 | 45 | public int newPage(byte[] initData) { 46 | int pgno = pageNumbers.incrementAndGet(); 47 | Page pg = new PageImpl(pgno, initData, null); 48 | flush(pg); 49 | return pgno; 50 | } 51 | 52 | public Page getPage(int pgno) throws Exception { 53 | return get((long)pgno); 54 | } 55 | 56 | /** 57 | * 根据pageNumber从数据库文件中读取页数据,并包裹成Page 58 | */ 59 | @Override 60 | protected Page getForCache(long key) throws Exception { 61 | int pgno = (int)key; 62 | long offset = PageCacheImpl.pageOffset(pgno); 63 | 64 | ByteBuffer buf = ByteBuffer.allocate(PAGE_SIZE); 65 | fileLock.lock(); 66 | try { 67 | fc.position(offset); 68 | fc.read(buf); 69 | } catch(IOException e) { 70 | Panic.panic(e); 71 | } 72 | fileLock.unlock(); 73 | return new PageImpl(pgno, buf.array(), this); 74 | } 75 | 76 | @Override 77 | protected void releaseForCache(Page pg) { 78 | if(pg.isDirty()) { 79 | flush(pg); 80 | pg.setDirty(false); 81 | } 82 | } 83 | 84 | public void release(Page page) { 85 | release((long)page.getPageNumber()); 86 | } 87 | 88 | public void flushPage(Page pg) { 89 | flush(pg); 90 | } 91 | 92 | private void flush(Page pg) { 93 | int pgno = pg.getPageNumber(); 94 | long offset = pageOffset(pgno); 95 | 96 | fileLock.lock(); 97 | try { 98 | ByteBuffer buf = ByteBuffer.wrap(pg.getData()); 99 | fc.position(offset); 100 | fc.write(buf); 101 | fc.force(false); 102 | } catch(IOException e) { 103 | Panic.panic(e); 104 | } finally { 105 | fileLock.unlock(); 106 | } 107 | } 108 | 109 | public void truncateByBgno(int maxPgno) { 110 | long size = pageOffset(maxPgno + 1); 111 | try { 112 | file.setLength(size); 113 | } catch (IOException e) { 114 | Panic.panic(e); 115 | } 116 | pageNumbers.set(maxPgno); 117 | } 118 | 119 | @Override 120 | public void close() { 121 | super.close(); 122 | try { 123 | fc.close(); 124 | file.close(); 125 | } catch (IOException e) { 126 | Panic.panic(e); 127 | } 128 | } 129 | 130 | public int getPageNumber() { 131 | return pageNumbers.intValue(); 132 | } 133 | 134 | private static long pageOffset(int pgno) { 135 | return (pgno-1) * PAGE_SIZE; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/im/UniqueIndexTest.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.im; 2 | 3 | import com.dyx.simpledb.backend.im.UniqueIndex; 4 | import org.junit.Test; 5 | 6 | /** 7 | * @User Administrator 8 | * @CreateTime 2024/9/5 15:28 9 | * @className com.dyx.simpledb.im.UniqueIndexTest 10 | */ 11 | public class UniqueIndexTest { 12 | 13 | @Test 14 | public void testInsertInTransaction() throws Exception { 15 | long xid = 1L; // 模拟事务ID 16 | UniqueIndex index = new UniqueIndex(); 17 | 18 | // 事务中插入数据 19 | index.insert("name", "Alice", xid); 20 | 21 | // 事务未提交时,数据不应出现在正式索引中 22 | assert !index.search("name", "Alice"); 23 | 24 | // 提交事务 25 | index.commit(xid); 26 | 27 | // 提交后,数据应出现在正式索引中 28 | assert index.search("name", "Alice"); 29 | } 30 | 31 | @Test 32 | public void testDeleteInTransaction() throws Exception { 33 | long xid = 2L; // 模拟事务ID 34 | UniqueIndex index = new UniqueIndex(); 35 | 36 | // 插入数据 37 | index.insert("name", "Bob", xid); 38 | index.commit(xid); 39 | 40 | // 在事务中删除数据 41 | long xid2 = 3L; // 新的事务 42 | index.delete("name", "Bob", xid2); 43 | 44 | // 事务未提交时,数据应仍然存在 45 | assert index.search("name", "Bob"); 46 | 47 | // 回滚事务,数据应恢复 48 | index.rollback(xid2); 49 | assert index.search("name", "Bob"); 50 | 51 | // 再次删除并提交事务 52 | index.delete("name", "Bob", xid2); 53 | index.commit(xid2); 54 | 55 | // 提交后,数据应被真正删除 56 | assert !index.search("name", "Bob"); 57 | } 58 | 59 | @Test 60 | public void testUpdateInTransaction() throws Exception { 61 | long xid = 4L; // 模拟事务ID 62 | UniqueIndex index = new UniqueIndex(); 63 | 64 | // 插入数据 65 | index.insert("name", "Charlie", xid); 66 | index.commit(xid); 67 | 68 | // 在事务中更新数据 69 | long xid2 = 5L; // 新的事务 70 | index.update("name", "Charlie", "David", xid2); 71 | 72 | // 事务未提交时,旧数据应存在,新数据暂未生效 73 | assert index.search("name", "Charlie"); 74 | assert !index.search("name", "David"); 75 | 76 | // 回滚事务,数据应恢复 77 | index.rollback(xid2); 78 | assert index.search("name", "Charlie"); 79 | assert !index.search("name", "David"); 80 | 81 | // 再次更新并提交事务 82 | index.update("name", "Charlie", "David", xid2); 83 | index.commit(xid2); 84 | 85 | // 提交后,旧数据应被删除,新数据应存在 86 | assert !index.search("name", "Charlie"); 87 | assert index.search("name", "David"); 88 | } 89 | 90 | @Test 91 | public void testRollback() throws Exception { 92 | UniqueIndex index = new UniqueIndex(); 93 | 94 | // 插入数据并提交事务 95 | long xid = 6L; // 模拟事务ID 96 | index.insert("name", "Eve", xid); 97 | index.commit(xid); // 提交事务 98 | 99 | // 事务中删除并回滚 100 | long xid2 = 7L; // 新的事务 101 | index.delete("name", "Eve", xid2); 102 | index.rollback(xid2); // 回滚删除操作 103 | 104 | // 回滚后,数据应恢复 105 | assert index.search("name", "Eve"); // Eve 应该存在 106 | 107 | // 事务中更新并回滚 108 | index.update("name", "Eve", "Frank", xid2); 109 | index.rollback(xid2); // 回滚更新操作 110 | 111 | // 回滚后,旧数据应恢复 112 | assert index.search("name", "Eve"); // Eve 应该恢复 113 | assert !index.search("name", "Frank"); // Frank 不应该存在 114 | } 115 | 116 | @Test 117 | public void testCommit() throws Exception { 118 | long xid = 8L; // 模拟事务ID 119 | UniqueIndex index = new UniqueIndex(); 120 | 121 | // 插入数据 122 | index.insert("name", "Grace", xid); 123 | index.commit(xid); 124 | 125 | // 提交后,数据应存在于唯一索引中 126 | assert index.search("name", "Grace"); 127 | 128 | // 事务中更新并提交 129 | long xid2 = 9L; // 新的事务 130 | index.update("name", "Grace", "Hank", xid2); 131 | index.commit(xid2); 132 | 133 | // 提交后,数据应更新 134 | assert !index.search("name", "Grace"); 135 | assert index.search("name", "Hank"); 136 | 137 | // 事务中删除并提交 138 | index.delete("name", "Hank", xid2); 139 | index.commit(xid2); 140 | 141 | // 提交后,数据应被删除 142 | assert !index.search("name", "Hank"); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/common/Error.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.common; 2 | 3 | public class Error { 4 | // Common 5 | public static final Exception CacheFullException = new RuntimeException("Cache is full: Unable to store more data due to cache capacity limits."); 6 | public static final Exception FileExistsException = new RuntimeException("File already exists: Cannot create a new file with the same name."); 7 | public static final Exception FileNotExistsException = new RuntimeException("File not found: The specified file does not exist."); 8 | public static final Exception FileCannotRWException = new RuntimeException("File access error: The file cannot be read from or written to due to permission or other issues."); 9 | 10 | // DM 11 | public static final Exception BadLogFileException = new RuntimeException("Log file corruption detected: The log file is invalid or corrupted."); 12 | public static final Exception MemTooSmallException = new RuntimeException("Insufficient memory: The allocated memory is too small for the operation."); 13 | public static final Exception DataTooLargeException = new RuntimeException("Data size exceeds limit: The provided data is too large to be processed."); 14 | public static final Exception DatabaseBusyException = new RuntimeException("Database is currently busy: Operation cannot proceed as the database is locked or in use."); 15 | 16 | // TM 17 | public static final Exception BadXIDFileException = new RuntimeException("XID file corruption detected: The transaction ID file is invalid or corrupted."); 18 | 19 | // VM 20 | public static final Exception DeadlockException = new RuntimeException("Deadlock detected: Two or more transactions are waiting indefinitely for resources held by each other."); 21 | public static final Exception TimeoutException = new RuntimeException("Transaction timeout: Lock wait exceeded the maximum allowed time; consider retrying the operation."); 22 | public static final Exception ConcurrentUpdateException = new RuntimeException("Concurrent modification error: Data has been modified by another transaction."); 23 | public static final Exception NullEntryException = new RuntimeException("Null value error: Attempted operation on a null entry."); 24 | 25 | // IM 26 | public static final Exception UniqueValuesNotRepeated = new RuntimeException("Unique values are not allowed to be repeated"); 27 | // TBM 28 | public static final Exception InvalidFieldException = new RuntimeException("Invalid field type: The field type provided does not match the expected type."); 29 | public static final Exception FieldNotFoundException = new RuntimeException("Field not found: The specified field does not exist in the table."); 30 | public static final Exception FieldNotIndexedException = new RuntimeException("Index missing: The field is not indexed, so the operation cannot proceed."); 31 | public static final Exception InvalidLogOpException = new RuntimeException("Invalid logical operation: The logical operation specified is not supported."); 32 | public static final Exception InvalidValuesException = new RuntimeException("Invalid input values: The provided values do not meet the expected criteria."); 33 | public static final Exception DuplicatedTableException = new RuntimeException("Table already exists: A table with the same name already exists in the database."); 34 | public static final Exception TableNotFoundException = new RuntimeException("Table not found: The specified table does not exist in the database."); 35 | public static final Exception TableNotCreateException = new RuntimeException("Table creation denied: The table name is restricted and cannot be used."); 36 | 37 | // Parser 38 | public static final Exception InvalidCommandException = new RuntimeException("Invalid command syntax: The command could not be parsed or is incorrect."); 39 | public static final Exception TableNoIndexException = new RuntimeException("Index missing: The table does not have an index for the requested operation."); 40 | 41 | // Transport 42 | public static final Exception InvalidPkgDataException = new RuntimeException("Package data error: The provided data format is invalid or corrupted."); 43 | 44 | // Server 45 | public static final Exception NestedTransactionException = new RuntimeException("Nested transaction not allowed: The system does not support transactions within transactions."); 46 | public static final Exception NoTransactionException = new RuntimeException("No active transaction: Attempted operation without an active transaction context."); 47 | 48 | // Launcher 49 | public static final Exception InvalidMemException = new RuntimeException("Memory allocation error: The specified memory configuration is invalid or inadequate."); 50 | } 51 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.7 9 | 10 | 11 | com.dyx.simpledb 12 | simple-sql-database 13 | 0.0.1-SNAPSHOT 14 | simple-sql-database 15 | simple-sql-database 16 | 17 | 8 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-logging 24 | 25 | 26 | 27 | org.slf4j 28 | slf4j-api 29 | 1.7.32 30 | 31 | 32 | 33 | cn.hutool 34 | hutool-all 35 | 5.7.16 36 | 37 | 38 | com.github.jsqlparser 39 | jsqlparser 40 | 4.5 41 | 42 | 43 | 44 | org.apache.calcite 45 | calcite-core 46 | 1.27.0 47 | 48 | 49 | org.apache.calcite 50 | calcite-server 51 | 1.27.0 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-websocket 57 | 58 | 59 | 60 | com.fasterxml.jackson.core 61 | jackson-databind 62 | 2.13.0 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-web 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-devtools 73 | true 74 | 75 | 76 | 77 | org.projectlombok 78 | lombok 79 | true 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-test 84 | test 85 | 86 | 87 | 88 | junit 89 | junit 90 | 4.13.2 91 | test 92 | 93 | 94 | com.google.guava 95 | guava 96 | 31.0.1-jre 97 | 98 | 99 | com.google.code.gson 100 | gson 101 | 2.8.9 102 | 103 | 104 | commons-codec 105 | commons-codec 106 | 1.15 107 | 108 | 109 | commons-cli 110 | commons-cli 111 | 1.5.0 112 | 113 | 114 | 115 | 116 | 117 | 118 | org.springframework.boot 119 | spring-boot-maven-plugin 120 | 121 | 122 | 123 | org.projectlombok 124 | lombok 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/parser/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.parser; 2 | 3 | import com.dyx.simpledb.common.Error; 4 | 5 | public class Tokenizer { 6 | private byte[] stat; 7 | private int pos; 8 | private String currentToken; 9 | private boolean flushToken; 10 | private Exception err; 11 | 12 | public Tokenizer(byte[] stat) { 13 | this.stat = stat; 14 | this.pos = 0; 15 | this.currentToken = ""; 16 | this.flushToken = true; 17 | } 18 | 19 | public String peek() throws Exception { 20 | if(err != null) { 21 | throw err; 22 | } 23 | if(flushToken) { 24 | String token = null; 25 | try { 26 | token = next(); 27 | } catch(Exception e) { 28 | err = e; 29 | throw e; 30 | } 31 | currentToken = token; 32 | flushToken = false; 33 | } 34 | return currentToken; 35 | } 36 | 37 | public void pop() { 38 | flushToken = true; 39 | } 40 | 41 | public byte[] errStat() { 42 | byte[] res = new byte[stat.length+3]; 43 | System.arraycopy(stat, 0, res, 0, pos); 44 | System.arraycopy("<< ".getBytes(), 0, res, pos, 3); 45 | System.arraycopy(stat, pos, res, pos+3, stat.length-pos); 46 | return res; 47 | } 48 | 49 | private void popByte() { 50 | pos ++; 51 | if(pos > stat.length) { 52 | pos = stat.length; 53 | } 54 | } 55 | 56 | private Byte peekByte() { 57 | if(pos == stat.length) { 58 | return null; 59 | } 60 | return stat[pos]; 61 | } 62 | 63 | private String next() throws Exception { 64 | if(err != null) { 65 | throw err; 66 | } 67 | return nextMetaState(); 68 | } 69 | 70 | private String nextMetaState() throws Exception { 71 | while(true) { 72 | Byte b = peekByte(); 73 | if(b == null) { 74 | return ""; 75 | } 76 | if(!isBlank(b)) { 77 | break; 78 | } 79 | popByte(); 80 | } 81 | byte b = peekByte(); 82 | if(isSymbol(b)) { 83 | popByte(); 84 | return new String(new byte[]{b}); 85 | } else if(b == '"' || b == '\'') { 86 | return nextQuoteState(); 87 | } else if(isAlphaBeta(b) || isDigit(b)) { 88 | return nextTokenState(); 89 | } else { 90 | err = Error.InvalidCommandException; 91 | throw err; 92 | } 93 | } 94 | 95 | // private String nextTokenState() throws Exception { 96 | // StringBuilder sb = new StringBuilder(); 97 | // while(true) { 98 | // Byte b = peekByte(); 99 | // if(b == null || !(isAlphaBeta(b) || isDigit(b) || b == '_')) { 100 | // if(b != null && isBlank(b)) { 101 | // popByte(); 102 | // } 103 | // return sb.toString(); 104 | // } 105 | // sb.append(new String(new byte[]{b})); 106 | // popByte(); 107 | // } 108 | // } 109 | 110 | static boolean isTokenPart(byte b) { 111 | return isAlphaBeta(b) || isDigit(b) || b == '_' || b == '.'; 112 | } 113 | private String nextTokenState() throws Exception { 114 | StringBuilder sb = new StringBuilder(); 115 | while(true) { 116 | Byte b = peekByte(); 117 | if(b == null || !isTokenPart(b)) { 118 | if(b != null && isBlank(b)) { 119 | popByte(); 120 | } 121 | return sb.toString(); 122 | } 123 | sb.append(new String(new byte[]{b})); 124 | popByte(); 125 | } 126 | } 127 | 128 | static boolean isNumberPart(byte b) { 129 | return isDigit(b) || b == '.' || b == '-'; 130 | } 131 | 132 | static boolean isDigit(byte b) { 133 | return (b >= '0' && b <= '9'); 134 | } 135 | 136 | static boolean isAlphaBeta(byte b) { 137 | return ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')); 138 | } 139 | 140 | private String nextQuoteState() throws Exception { 141 | byte quote = peekByte(); 142 | popByte(); 143 | StringBuilder sb = new StringBuilder(); 144 | while(true) { 145 | Byte b = peekByte(); 146 | if(b == null) { 147 | err = Error.InvalidCommandException; 148 | throw err; 149 | } 150 | if(b == quote) { 151 | popByte(); 152 | break; 153 | } 154 | sb.append(new String(new byte[]{b})); 155 | popByte(); 156 | } 157 | return sb.toString(); 158 | } 159 | 160 | static boolean isSymbol(byte b) { 161 | return (b == '>' || b == '<' || b == '=' || b == '*' || 162 | b == ',' || b == '(' || b == ')'); 163 | } 164 | 165 | static boolean isBlank(byte b) { 166 | return (b == '\n' || b == ' ' || b == '\t'); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/parser/ParserTest.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.parser; 2 | 3 | import com.dyx.simpledb.backend.parser.Parser; 4 | import com.dyx.simpledb.backend.parser.statement.Create; 5 | import net.sf.jsqlparser.JSQLParserException; 6 | import net.sf.jsqlparser.parser.CCJSqlParserUtil; 7 | import net.sf.jsqlparser.statement.Statement; 8 | import net.sf.jsqlparser.statement.create.table.ColumnDefinition; 9 | import net.sf.jsqlparser.statement.create.table.CreateTable; 10 | import net.sf.jsqlparser.statement.create.table.Index; 11 | import net.sf.jsqlparser.statement.create.table.NamedConstraint; 12 | import org.junit.Test; 13 | import org.mockito.internal.util.collections.ListUtil; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | 23 | /** 24 | * @User Administrator 25 | * @CreateTime 2024/7/24 15:00 26 | * @className com.dyx.simpledb.parser.ParserTest 27 | */ 28 | public class ParserTest { 29 | 30 | @Test 31 | public void select() throws Exception { 32 | String sql = "SELECT user.id, user.name, orders.order_id FROM user INNER JOIN orders ON user.id = orders.user_id WHERE user.id > 10 AND user.name = 'zhangsan';"; 33 | System.out.println(Parser.Parse(sql.getBytes())); 34 | } 35 | 36 | @Test 37 | public void update() throws Exception { 38 | String sql = "UPDATE user SET name = 'lisi',email = 'zhangsan@qq.com' WHERE id = 1;"; 39 | System.out.println(Parser.Parse(sql.getBytes())); 40 | } 41 | 42 | @Test 43 | public void delete() throws Exception { 44 | String sql = "DELETE FROM user WHERE id = 1;"; 45 | System.out.println(Parser.Parse(sql.getBytes())); 46 | } 47 | 48 | @Test 49 | public void testParseCreate() throws Exception { 50 | // Create a mock SQL create table statement 51 | String sql = "CREATE TABLE employee (" + 52 | "employee_id INT AUTO_INCREMENT PRIMARY_KEY, " + 53 | "name VARCHAR(255) NOT_NULL, " + 54 | "email VARCHAR(255), " + 55 | "INDEX idx_email (email)" + 56 | ")"; 57 | 58 | // Parse the SQL create table statement 59 | CreateTable createTable = (CreateTable) CCJSqlParserUtil.parse(sql); 60 | Create create = parseCreate(createTable); 61 | 62 | System.out.println(create); 63 | } 64 | 65 | private static Create parseCreate(CreateTable createTable) { 66 | Create create = new Create(); 67 | create.tableName = createTable.getTable().getName(); 68 | List fieldNames = new ArrayList<>(); 69 | List fieldTypes = new ArrayList<>(); 70 | List indexes = new ArrayList<>(); 71 | List primaryKey = new ArrayList<>(); 72 | List autoIncrement = new ArrayList<>(); 73 | List notNull = new ArrayList<>(); 74 | 75 | 76 | for (ColumnDefinition columnDefinition : createTable.getColumnDefinitions()) { 77 | fieldNames.add(columnDefinition.getColumnName()); 78 | fieldTypes.add(columnDefinition.getColDataType().toString()); 79 | 80 | if (columnDefinition.getColumnSpecs() != null) { 81 | for (String columnSpec : columnDefinition.getColumnSpecs()) { 82 | if (columnSpec.equalsIgnoreCase("PRIMARY_KEY")) { 83 | primaryKey.add(columnDefinition.getColumnName()); 84 | } else if (columnSpec.equalsIgnoreCase("AUTO_INCREMENT")) { 85 | autoIncrement.add(columnDefinition.getColumnName()); 86 | } else if (columnSpec.equalsIgnoreCase("NOT_NULL")) { 87 | notNull.add(columnDefinition.getColumnName()); 88 | } 89 | } 90 | } 91 | } 92 | 93 | if (createTable.getIndexes() != null) { 94 | for (Index index : createTable.getIndexes()) { 95 | // 只处理单列索引 96 | if (index.getColumnsNames().size() == 1) { 97 | indexes.add(index.getColumnsNames().get(0)); 98 | } 99 | } 100 | } 101 | 102 | create.fieldName = fieldNames.toArray(new String[0]); 103 | create.fieldType = fieldTypes.toArray(new String[0]); 104 | create.index = indexes.toArray(new String[0]); 105 | create.autoIncrement = autoIncrement.toArray(new String[0]); 106 | create.notNull = notNull.toArray(new String[0]); 107 | 108 | return create; 109 | } 110 | 111 | @Test 112 | public void insert() throws Exception { 113 | String sql = "INSERT INTO user VALUES (\"zhangsan\");"; 114 | System.out.println(Parser.Parse(sql.getBytes())); 115 | } 116 | 117 | @Test 118 | public void begin() throws Exception { 119 | String sql1 = "BEGIN isolation level read committed"; 120 | String sql2 = "BEGIN isolation level repeatable read"; 121 | String sql3 = "BEGIN"; 122 | 123 | System.out.println(Parser.Parse(sql1.getBytes())); 124 | System.out.println(Parser.Parse(sql2.getBytes())); 125 | System.out.println(Parser.Parse(sql3.getBytes())); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tm/TransactionManagerImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tm; 2 | 3 | import java.io.IOException; 4 | import java.io.RandomAccessFile; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.FileChannel; 7 | import java.util.concurrent.locks.Lock; 8 | import java.util.concurrent.locks.ReentrantLock; 9 | 10 | import com.dyx.simpledb.backend.utils.Panic; 11 | import com.dyx.simpledb.backend.utils.Parser; 12 | import com.dyx.simpledb.common.Error; 13 | 14 | public class TransactionManagerImpl implements TransactionManager { 15 | 16 | // XID文件头长度 17 | static final int LEN_XID_HEADER_LENGTH = 8; 18 | // 每个事务的占用长度 19 | private static final int XID_FIELD_SIZE = 1; 20 | 21 | // 事务的三种状态 22 | private static final byte FIELD_TRAN_ACTIVE = 0; 23 | private static final byte FIELD_TRAN_COMMITTED = 1; 24 | private static final byte FIELD_TRAN_ABORTED = 2; 25 | 26 | // 超级事务,永远为commited状态 27 | public static final long SUPER_XID = 0; 28 | 29 | static final String XID_SUFFIX = ".xid"; 30 | 31 | private RandomAccessFile file; 32 | private FileChannel fc; 33 | private long xidCounter; 34 | private Lock counterLock; 35 | 36 | TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) { 37 | this.file = raf; 38 | this.fc = fc; 39 | counterLock = new ReentrantLock(); 40 | checkXIDCounter(); 41 | } 42 | 43 | /** 44 | * 检查XID文件是否合法 45 | * 读取XID_FILE_HEADER中的xidcounter,根据它计算文件的理论长度,对比实际长度 46 | */ 47 | private void checkXIDCounter() { 48 | long fileLen = 0; 49 | try { 50 | fileLen = file.length(); 51 | } catch (IOException e1) { 52 | Panic.panic(Error.BadXIDFileException); 53 | } 54 | if(fileLen < LEN_XID_HEADER_LENGTH) { 55 | Panic.panic(Error.BadXIDFileException); 56 | } 57 | 58 | ByteBuffer buf = ByteBuffer.allocate(LEN_XID_HEADER_LENGTH); 59 | try { 60 | fc.position(0); 61 | fc.read(buf); 62 | } catch (IOException e) { 63 | Panic.panic(e); 64 | } 65 | this.xidCounter = Parser.parseLong(buf.array()); 66 | long end = getXidPosition(this.xidCounter + 1); 67 | if(end != fileLen) { 68 | Panic.panic(Error.BadXIDFileException); 69 | } 70 | } 71 | 72 | // 根据事务xid取得其在xid文件中对应的位置 73 | private long getXidPosition(long xid) { 74 | return LEN_XID_HEADER_LENGTH + (xid-1)*XID_FIELD_SIZE; 75 | } 76 | 77 | // 更新xid事务的状态为status 78 | private void updateXID(long xid, byte status) { 79 | long offset = getXidPosition(xid); 80 | byte[] tmp = new byte[XID_FIELD_SIZE]; 81 | tmp[0] = status; 82 | ByteBuffer buf = ByteBuffer.wrap(tmp); 83 | try { 84 | fc.position(offset); 85 | fc.write(buf); 86 | } catch (IOException e) { 87 | Panic.panic(e); 88 | } 89 | try { 90 | fc.force(false); 91 | } catch (IOException e) { 92 | Panic.panic(e); 93 | } 94 | } 95 | 96 | // 将XID加一,并更新XID Header 97 | private void incrXIDCounter() { 98 | xidCounter ++; 99 | ByteBuffer buf = ByteBuffer.wrap(Parser.long2Byte(xidCounter)); 100 | try { 101 | fc.position(0); 102 | fc.write(buf); 103 | } catch (IOException e) { 104 | Panic.panic(e); 105 | } 106 | try { 107 | fc.force(false); 108 | } catch (IOException e) { 109 | Panic.panic(e); 110 | } 111 | } 112 | 113 | // 开始一个事务,并返回XID 114 | public long begin() { 115 | counterLock.lock(); 116 | try { 117 | long xid = xidCounter + 1; 118 | updateXID(xid, FIELD_TRAN_ACTIVE); 119 | incrXIDCounter(); 120 | return xid; 121 | } finally { 122 | counterLock.unlock(); 123 | } 124 | } 125 | 126 | // 提交XID事务 127 | public void commit(long xid) { 128 | updateXID(xid, FIELD_TRAN_COMMITTED); 129 | } 130 | 131 | // 回滚XID事务 132 | public void abort(long xid) { 133 | updateXID(xid, FIELD_TRAN_ABORTED); 134 | } 135 | 136 | // 检测XID事务是否处于status状态 137 | private boolean checkXID(long xid, byte status) { 138 | long offset = getXidPosition(xid); 139 | ByteBuffer buf = ByteBuffer.wrap(new byte[XID_FIELD_SIZE]); 140 | try { 141 | fc.position(offset); 142 | fc.read(buf); 143 | } catch (IOException e) { 144 | Panic.panic(e); 145 | } 146 | return buf.array()[0] == status; 147 | } 148 | 149 | public boolean isActive(long xid) { 150 | if(xid == SUPER_XID) return false; 151 | return checkXID(xid, FIELD_TRAN_ACTIVE); 152 | } 153 | 154 | public boolean isCommitted(long xid) { 155 | if(xid == SUPER_XID) return true; 156 | return checkXID(xid, FIELD_TRAN_COMMITTED); 157 | } 158 | 159 | public boolean isAborted(long xid) { 160 | if(xid == SUPER_XID) return false; 161 | return checkXID(xid, FIELD_TRAN_ABORTED); 162 | } 163 | 164 | public void close() { 165 | try { 166 | fc.close(); 167 | file.close(); 168 | } catch (IOException e) { 169 | Panic.panic(e); 170 | } 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/DataManagerImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm; 2 | 3 | import com.dyx.simpledb.backend.common.AbstractCache; 4 | import com.dyx.simpledb.backend.dm.dataItem.DataItem; 5 | import com.dyx.simpledb.backend.dm.dataItem.DataItemImpl; 6 | import com.dyx.simpledb.backend.dm.logger.Logger; 7 | import com.dyx.simpledb.backend.dm.page.Page; 8 | import com.dyx.simpledb.backend.dm.page.PageOne; 9 | import com.dyx.simpledb.backend.dm.page.PageX; 10 | import com.dyx.simpledb.backend.dm.pageCache.PageCache; 11 | import com.dyx.simpledb.backend.dm.pageIndex.PageIndex; 12 | import com.dyx.simpledb.backend.dm.pageIndex.PageInfo; 13 | import com.dyx.simpledb.backend.tm.TransactionManager; 14 | import com.dyx.simpledb.backend.utils.Panic; 15 | import com.dyx.simpledb.backend.utils.Parser; 16 | import com.dyx.simpledb.backend.utils.Types; 17 | import com.dyx.simpledb.common.Error; 18 | 19 | import java.util.Arrays; 20 | 21 | public class DataManagerImpl extends AbstractCache implements DataManager { 22 | 23 | TransactionManager tm; 24 | PageCache pc; 25 | Logger logger; 26 | PageIndex pIndex; 27 | Page pageOne; 28 | 29 | public DataManagerImpl(PageCache pc, Logger logger, TransactionManager tm) { 30 | super(0); 31 | this.pc = pc; 32 | this.logger = logger; 33 | this.tm = tm; 34 | this.pIndex = new PageIndex(); 35 | } 36 | 37 | @Override 38 | public DataItem read(long uid) throws Exception { 39 | DataItemImpl di = (DataItemImpl)super.get(uid); 40 | if(!di.isValid()) { 41 | di.release(); 42 | return null; 43 | } 44 | return di; 45 | } 46 | 47 | @Override 48 | public long insert(long xid, byte[] data) throws Exception { 49 | byte[] raw = DataItem.wrapDataItemRaw(data); 50 | if(raw.length > PageX.MAX_FREE_SPACE) { 51 | throw Error.DataTooLargeException; 52 | } 53 | 54 | PageInfo pi = null; 55 | for(int i = 0; i < 5; i ++) { 56 | pi = pIndex.select(raw.length); 57 | if (pi != null) { 58 | break; 59 | } else { 60 | int newPgno = pc.newPage(PageX.initRaw()); 61 | pIndex.add(newPgno, PageX.MAX_FREE_SPACE); 62 | } 63 | } 64 | if(pi == null) { 65 | throw Error.DatabaseBusyException; 66 | } 67 | 68 | Page pg = null; 69 | int freeSpace = 0; 70 | try { 71 | pg = pc.getPage(pi.pgno); 72 | byte[] log = Recover.insertLog(xid, pg, raw); 73 | logger.log(log); 74 | 75 | short offset = PageX.insert(pg, raw); 76 | 77 | pg.release(); 78 | return Types.addressToUid(pi.pgno, offset); 79 | 80 | } finally { 81 | // 将取出的pg重新插入pIndex 82 | if(pg != null) { 83 | pIndex.add(pi.pgno, PageX.getFreeSpace(pg)); 84 | } else { 85 | pIndex.add(pi.pgno, freeSpace); 86 | } 87 | } 88 | } 89 | 90 | @Override 91 | public void close() { 92 | super.close(); 93 | logger.close(); 94 | 95 | PageOne.setVcClose(pageOne); 96 | pageOne.release(); 97 | pc.close(); 98 | } 99 | 100 | @Override 101 | public void physicalDelete(Long uid) throws Exception { 102 | // 解析出页号和偏移量 103 | short offset = (short) (uid & ((1L << 16) - 1)); 104 | uid >>>= 32; 105 | int pgno = (int) (uid & ((1L << 32) - 1)); 106 | 107 | // 获取目标页 108 | Page pg = pc.getPage(pgno); 109 | try { 110 | // 获取页中的数据 111 | byte[] data = pg.getData(); 112 | 113 | // 计算数据项的大小 114 | short size = Parser.parseShort(Arrays.copyOfRange(data, offset + DataItemImpl.OF_SIZE, offset + DataItemImpl.OF_DATA)); 115 | int dataItemLength = DataItemImpl.OF_DATA + size; 116 | 117 | // 清除数据项的内容(将数据项所在区域的字节清零) 118 | Arrays.fill(data, offset, offset + dataItemLength, (byte) 0); 119 | 120 | // 更新该页的可用空间信息 121 | pIndex.add(pgno, PageX.getFreeSpace(pg)); 122 | 123 | } finally { 124 | // 释放页 125 | pg.release(); 126 | } 127 | } 128 | 129 | // 为xid生成update日志 130 | public void logDataItem(long xid, DataItem di) { 131 | byte[] log = Recover.updateLog(xid, di); 132 | logger.log(log); 133 | } 134 | 135 | public void releaseDataItem(DataItem di) { 136 | super.release(di.getUid()); 137 | } 138 | 139 | @Override 140 | protected DataItem getForCache(long uid) throws Exception { 141 | short offset = (short)(uid & ((1L << 16) - 1)); 142 | uid >>>= 32; 143 | int pgno = (int)(uid & ((1L << 32) - 1)); 144 | Page pg = pc.getPage(pgno); 145 | return DataItem.parseDataItem(pg, offset, this); 146 | } 147 | 148 | @Override 149 | protected void releaseForCache(DataItem di) { 150 | di.page().release(); 151 | } 152 | 153 | // 在创建文件时初始化PageOne 154 | void initPageOne() { 155 | int pgno = pc.newPage(PageOne.InitRaw()); 156 | assert pgno == 1; 157 | try { 158 | pageOne = pc.getPage(pgno); 159 | } catch (Exception e) { 160 | Panic.panic(e); 161 | } 162 | pc.flushPage(pageOne); 163 | } 164 | 165 | // 在打开已有文件时时读入PageOne,并验证正确性 166 | boolean loadCheckPageOne() { 167 | try { 168 | pageOne = pc.getPage(1); 169 | } catch (Exception e) { 170 | Panic.panic(e); 171 | } 172 | return PageOne.checkVc(pageOne); 173 | } 174 | 175 | // 初始化pageIndex 176 | void fillPageIndex() { 177 | int pageNumber = pc.getPageNumber(); 178 | for(int i = 2; i <= pageNumber; i ++) { 179 | Page pg = null; 180 | try { 181 | pg = pc.getPage(i); 182 | } catch (Exception e) { 183 | Panic.panic(e); 184 | } 185 | pIndex.add(pg.getPageNumber(), PageX.getFreeSpace(pg)); 186 | pg.release(); 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/im/UniqueIndex.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.im; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.concurrent.locks.Lock; 8 | import java.util.concurrent.locks.ReentrantLock; 9 | 10 | public class UniqueIndex { 11 | 12 | // 正式的唯一索引:已提交的数据 13 | private Map> index; 14 | 15 | // 临时数据:用于存储每个事务的插入或删除操作 16 | private Map>> tempIndex; 17 | 18 | // 删除标记:记录哪些数据在事务中被删除(用于回滚恢复) 19 | private Map>> deleteMark; 20 | private Lock indexLock; 21 | 22 | // 构造方法 23 | public UniqueIndex() { 24 | index = new HashMap<>(); 25 | tempIndex = new HashMap<>(); 26 | deleteMark = new HashMap<>(); 27 | indexLock = new ReentrantLock(); 28 | } 29 | 30 | // 插入操作,确保唯一性 31 | public synchronized void insert(String column, Object data, long xid) throws Exception { 32 | indexLock.lock(); 33 | try { 34 | index.putIfAbsent(column, new HashSet<>()); 35 | tempIndex.putIfAbsent(xid, new HashMap<>()); 36 | tempIndex.get(xid).putIfAbsent(column, new HashSet<>()); 37 | 38 | // 检查正式索引、事务中的临时数据、其他事务的临时数据 39 | for (Map.Entry>> entry : tempIndex.entrySet()) { 40 | Long otherXid = entry.getKey(); 41 | if (!otherXid.equals(xid)) { 42 | // 检查其他事务中是否有相同的数据 43 | if (tempIndex.get(otherXid).getOrDefault(column, new HashSet<>()).contains(data)) { 44 | throw new Exception("Unique constraint violation. Data already exists in another active transaction."); 45 | } 46 | } 47 | } 48 | 49 | // 检查正式索引是否已经有该数据 50 | if (index.get(column).contains(data) || tempIndex.get(xid).get(column).contains(data)) { 51 | throw new Exception("Unique constraint violation. Data already exists in this transaction."); 52 | } 53 | 54 | // 插入事务临时数据 55 | tempIndex.get(xid).get(column).add(data); 56 | } finally { 57 | indexLock.unlock(); 58 | } 59 | } 60 | 61 | // 删除操作,支持事务中的删除,并标记待删除的数据 62 | public void delete(String column, Object data, long xid) throws Exception { 63 | tempIndex.putIfAbsent(xid, new HashMap<>()); 64 | deleteMark.putIfAbsent(xid, new HashMap<>()); 65 | deleteMark.get(xid).putIfAbsent(column, new HashSet<>()); 66 | 67 | // 优先检查正式索引并标记删除 68 | if (index.containsKey(column) && index.get(column).contains(data)) { 69 | deleteMark.get(xid).get(column).add(data); // 标记为删除 70 | } else if (tempIndex.get(xid).containsKey(column) && tempIndex.get(xid).get(column).contains(data)) { 71 | // 检查事务中的临时数据并删除 72 | tempIndex.get(xid).get(column).remove(data); 73 | } else { 74 | throw new Exception("Data not found in the column for deletion."); 75 | } 76 | } 77 | 78 | 79 | // 提交操作:提交事务中的修改到正式索引中 80 | // 提交操作:提交事务中的修改到正式索引中 81 | public synchronized void commit(long xid) throws Exception { 82 | indexLock.lock(); 83 | try { 84 | if (!tempIndex.containsKey(xid)) return; // 如果没有修改则直接返回 85 | 86 | // 检查冲突 87 | for (String column : tempIndex.get(xid).keySet()) { 88 | for (Object data : tempIndex.get(xid).get(column)) { 89 | if (index.get(column).contains(data)) { 90 | throw new Exception("Unique constraint violation during commit. Data already exists."); 91 | } 92 | } 93 | } 94 | 95 | // 合并临时数据到正式索引中 96 | for (String column : tempIndex.get(xid).keySet()) { 97 | index.putIfAbsent(column, new HashSet<>()); 98 | index.get(column).addAll(tempIndex.get(xid).get(column)); 99 | } 100 | 101 | // 从正式索引中删除在 deleteMark 中标记为删除的数据 102 | if (deleteMark.containsKey(xid)) { 103 | for (String column : deleteMark.get(xid).keySet()) { 104 | index.get(column).removeAll(deleteMark.get(xid).get(column)); // 执行正式删除 105 | } 106 | } 107 | 108 | // 清除事务临时数据和删除标记 109 | tempIndex.remove(xid); 110 | deleteMark.remove(xid); 111 | 112 | } finally { 113 | indexLock.unlock(); 114 | } 115 | } 116 | 117 | // 回滚操作:撤销事务中的操作 118 | public void rollback(long xid) { 119 | indexLock.lock(); 120 | try { 121 | if (!tempIndex.containsKey(xid)) return; 122 | 123 | // 撤销插入操作:事务中的数据不合并到正式索引 124 | tempIndex.remove(xid); 125 | 126 | // 恢复删除的数据:撤销事务中的删除标记 127 | if (deleteMark.containsKey(xid)) { 128 | for (String column : deleteMark.get(xid).keySet()) { 129 | for (Object data : deleteMark.get(xid).get(column)) { 130 | index.putIfAbsent(column, new HashSet<>()); 131 | index.get(column).add(data); // 恢复删除的数据 132 | } 133 | } 134 | } 135 | 136 | // 清除删除标记 137 | deleteMark.remove(xid); 138 | 139 | } finally { 140 | indexLock.unlock(); 141 | } 142 | } 143 | 144 | 145 | // 查询操作:检查唯一索引中某个列是否包含指定数据 146 | public boolean search(String column, Object data) { 147 | // 如果该列不存在,返回 false 148 | if (!index.containsKey(column)) { 149 | return false; 150 | } 151 | return index.get(column).contains(data); // 检查正式索引是否包含数据 152 | } 153 | 154 | // 更新操作:先删除旧数据,再插入新数据 155 | public void update(String column, Object oldData, Object newData, long xid) throws Exception { 156 | delete(column, oldData, xid); 157 | insert(column, newData, xid); 158 | } 159 | 160 | // 删除列索引的方法 161 | public void remove(String column) { 162 | index.remove(column); 163 | } 164 | 165 | // 显示哈希表内容(调试用) 166 | public void display() { 167 | System.out.println("Main Index:"); 168 | for (String column : index.keySet()) { 169 | System.out.println("Column: " + column + ", Values: " + index.get(column)); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/com/dyx/simpledb/vm/LockTableTest.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.vm; 2 | 3 | import com.dyx.simpledb.backend.vm.LockTable; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.util.concurrent.locks.Lock; 9 | 10 | @Log4j2 11 | public class LockTableTest { 12 | private LockTable lockTable; 13 | 14 | @Before 15 | public void setUp() { 16 | lockTable = new LockTable(); 17 | } 18 | 19 | @Test 20 | public void testDeadlockAndTimeout() { 21 | try { 22 | // 启动两个线程以创建死锁 23 | new Thread(() -> { 24 | try { 25 | long xid1 = 1L; 26 | long resourceA = 100L; 27 | long resourceB = 200L; 28 | 29 | log.info("Transaction " + xid1 + " trying to acquire resource A."); 30 | Lock lockA = lockTable.add(xid1, resourceA); 31 | 32 | if (lockA != null) { 33 | log.info("Transaction " + xid1 + " acquired resource A."); 34 | } 35 | 36 | // Simulate some work 37 | Thread.sleep(1000); 38 | 39 | log.info("Transaction " + xid1 + " trying to acquire resource B."); 40 | Lock lockB = lockTable.add(xid1, resourceB); 41 | 42 | if (lockB != null) { 43 | log.info("Transaction " + xid1 + " acquired resource B."); 44 | } else { 45 | log.info("Transaction " + xid1 + " is waiting for resource B."); 46 | } 47 | 48 | // Simulate some more work 49 | Thread.sleep(10000); 50 | 51 | lockTable.remove(xid1); 52 | log.info("Transaction " + xid1 + " completed."); 53 | 54 | } catch (Exception e) { 55 | log.error("Error in transaction", e); 56 | } 57 | }).start(); 58 | 59 | new Thread(() -> { 60 | try { 61 | long xid2 = 2L; 62 | long resourceA = 100L; 63 | long resourceB = 200L; 64 | 65 | log.info("Transaction " + xid2 + " trying to acquire resource B."); 66 | Lock lockB = lockTable.add(xid2, resourceB); 67 | 68 | if (lockB != null) { 69 | log.info("Transaction " + xid2 + " acquired resource B."); 70 | } 71 | 72 | // Simulate some work 73 | Thread.sleep(1000); 74 | 75 | log.info("Transaction " + xid2 + " trying to acquire resource A."); 76 | Lock lockA = lockTable.add(xid2, resourceA); 77 | 78 | if (lockA != null) { 79 | log.info("Transaction " + xid2 + " acquired resource A."); 80 | } else { 81 | log.info("Transaction " + xid2 + " is waiting for resource A."); 82 | } 83 | 84 | // Simulate some more work 85 | Thread.sleep(10000); 86 | 87 | lockTable.remove(xid2); 88 | log.info("Transaction " + xid2 + " completed."); 89 | 90 | } catch (Exception e) { 91 | log.error("Error in transaction", e); 92 | } 93 | }).start(); 94 | 95 | // 等待足够的时间以确保死锁发生 96 | Thread.sleep(20000); 97 | 98 | } catch (Exception e) { 99 | log.error("Error in deadlock and timeout test", e); 100 | } 101 | } 102 | 103 | @Test 104 | public void checkTimeout(){ 105 | LockTable lockTable = new LockTable(); 106 | 107 | // 创建几个模拟的事务ID和资源ID 108 | long xid1 = 1L; 109 | long xid2 = 2L; 110 | long xid3 = 3L; 111 | long uid1 = 100L; 112 | long uid2 = 200L; 113 | 114 | // 测试1:事务1获取资源1 115 | try { 116 | Lock lock1 = lockTable.add(xid1, uid1); 117 | System.out.println("Transaction " + xid1 + " acquired resource " + uid1); 118 | if (lock1 != null) { 119 | lock1.unlock(); 120 | } 121 | } catch (Exception e) { 122 | System.out.println("Transaction " + xid1 + " failed to acquire resource " + uid1); 123 | } 124 | 125 | // 测试2:事务2尝试获取已经被占用的资源1,这将导致它进入等待状态 126 | try { 127 | Lock lock2 = lockTable.add(xid2, uid1); 128 | System.out.println("Transaction " + xid2 + " is waiting to acquire resource " + uid1); 129 | } catch (Exception e) { 130 | System.out.println("Transaction " + xid2 + " failed to acquire resource " + uid1); 131 | } 132 | 133 | // 测试3:事务3获取资源2,不冲突 134 | try { 135 | Lock lock3 = lockTable.add(xid3, uid2); 136 | System.out.println("Transaction " + xid3 + " acquired resource " + uid2); 137 | if (lock3 != null) { 138 | lock3.unlock(); 139 | } 140 | } catch (Exception e) { 141 | System.out.println("Transaction " + xid3 + " failed to acquire resource " + uid2); 142 | } 143 | 144 | // 模拟事务2超时的情景,等待时间超过设定的阈值 145 | try { 146 | Thread.sleep(35000); // 35秒后,事务2应该被回滚,因为它等待超过了30秒的阈值 147 | } catch (InterruptedException e) { 148 | e.printStackTrace(); 149 | } 150 | 151 | // 测试4:检查事务2是否超时并被回滚 152 | try { 153 | lockTable.remove(xid2); 154 | System.out.println("Transaction " + xid2 + " has been successfully rolled back due to timeout."); 155 | } catch (Exception e) { 156 | System.out.println("Failed to roll back transaction " + xid2); 157 | } 158 | 159 | // 测试5:事务1释放资源1后,事务2重新获取资源1 160 | try { 161 | lockTable.remove(xid1); // 释放资源1 162 | System.out.println("Transaction " + xid1 + " released resource " + uid1); 163 | 164 | // 事务2应该在此时获取到资源1 165 | Lock lock2 = lockTable.add(xid2, uid1); 166 | if (lock2 != null) { 167 | lock2.unlock(); 168 | System.out.println("Transaction " + xid2 + " acquired resource " + uid1 + " after xid1 released it."); 169 | } 170 | } catch (Exception e) { 171 | System.out.println("Transaction " + xid2 + " failed to acquire resource " + uid1); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/logger/LoggerImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm.logger; 2 | 3 | import java.io.IOException; 4 | import java.io.RandomAccessFile; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.FileChannel; 7 | import java.util.Arrays; 8 | import java.util.concurrent.locks.Lock; 9 | import java.util.concurrent.locks.ReentrantLock; 10 | 11 | import com.google.common.primitives.Bytes; 12 | 13 | import com.dyx.simpledb.backend.utils.Panic; 14 | import com.dyx.simpledb.backend.utils.Parser; 15 | import com.dyx.simpledb.common.Error; 16 | 17 | /** 18 | * 日志文件读写 19 | * 20 | * 日志文件标准格式为: 21 | * [XChecksum] [Log1] [Log2] ... [LogN] [BadTail] 22 | * XChecksum 为后续所有日志计算的Checksum,int类型 23 | * 24 | * 每条正确日志的格式为: 25 | * [Size] [Checksum] [Data] 26 | * Size 4字节int 标识Data长度 27 | * Checksum 4字节int 28 | */ 29 | public class LoggerImpl implements Logger { 30 | 31 | private static final int SEED = 13331; 32 | 33 | private static final int OF_SIZE = 0; 34 | private static final int OF_CHECKSUM = OF_SIZE + 4; 35 | private static final int OF_DATA = OF_CHECKSUM + 4; 36 | 37 | public static final String LOG_SUFFIX = ".log"; 38 | 39 | private RandomAccessFile file; 40 | private FileChannel fc; 41 | private Lock lock; 42 | 43 | private long position; // 当前日志指针的位置 44 | private long fileSize; // 初始化时记录,log操作不更新 45 | private int xChecksum; 46 | 47 | LoggerImpl(RandomAccessFile raf, FileChannel fc) { 48 | this.file = raf; 49 | this.fc = fc; 50 | lock = new ReentrantLock(); 51 | } 52 | 53 | LoggerImpl(RandomAccessFile raf, FileChannel fc, int xChecksum) { 54 | this.file = raf; 55 | this.fc = fc; 56 | this.xChecksum = xChecksum; 57 | lock = new ReentrantLock(); 58 | } 59 | 60 | void init() { 61 | long size = 0; 62 | try { 63 | size = file.length(); 64 | } catch (IOException e) { 65 | Panic.panic(e); 66 | } 67 | if(size < 4) { 68 | Panic.panic(Error.BadLogFileException); 69 | } 70 | 71 | ByteBuffer raw = ByteBuffer.allocate(4); 72 | try { 73 | fc.position(0); 74 | fc.read(raw); 75 | } catch (IOException e) { 76 | Panic.panic(e); 77 | } 78 | int xChecksum = Parser.parseInt(raw.array()); 79 | this.fileSize = size; 80 | this.xChecksum = xChecksum; 81 | 82 | checkAndRemoveTail(); 83 | } 84 | 85 | // 检查并移除bad tail 86 | private void checkAndRemoveTail() { 87 | rewind(); 88 | 89 | int xCheck = 0; 90 | while(true) { 91 | byte[] log = internNext(); 92 | if(log == null) break; 93 | xCheck = calChecksum(xCheck, log); 94 | } 95 | if(xCheck != xChecksum) { 96 | Panic.panic(Error.BadLogFileException); 97 | } 98 | 99 | try { 100 | truncate(position); 101 | } catch (Exception e) { 102 | Panic.panic(e); 103 | } 104 | try { 105 | file.seek(position); 106 | } catch (IOException e) { 107 | Panic.panic(e); 108 | } 109 | rewind(); 110 | } 111 | 112 | private int calChecksum(int xCheck, byte[] log) { 113 | for (byte b : log) { 114 | xCheck = xCheck * SEED + b; 115 | } 116 | return xCheck; 117 | } 118 | 119 | @Override 120 | public void log(byte[] data) { 121 | byte[] log = wrapLog(data); 122 | ByteBuffer buf = ByteBuffer.wrap(log); 123 | lock.lock(); 124 | try { 125 | fc.position(fc.size()); 126 | fc.write(buf); 127 | } catch(IOException e) { 128 | Panic.panic(e); 129 | } finally { 130 | lock.unlock(); 131 | } 132 | updateXChecksum(log); 133 | } 134 | 135 | private void updateXChecksum(byte[] log) { 136 | this.xChecksum = calChecksum(this.xChecksum, log); 137 | try { 138 | fc.position(0); 139 | fc.write(ByteBuffer.wrap(Parser.int2Byte(xChecksum))); 140 | fc.force(false); 141 | } catch(IOException e) { 142 | Panic.panic(e); 143 | } 144 | } 145 | 146 | private byte[] wrapLog(byte[] data) { 147 | byte[] checksum = Parser.int2Byte(calChecksum(0, data)); 148 | byte[] size = Parser.int2Byte(data.length); 149 | return Bytes.concat(size, checksum, data); 150 | } 151 | 152 | @Override 153 | public void truncate(long x) throws Exception { 154 | lock.lock(); 155 | try { 156 | fc.truncate(x); 157 | } finally { 158 | lock.unlock(); 159 | } 160 | } 161 | 162 | private byte[] internNext() { 163 | if(position + OF_DATA >= fileSize) { 164 | return null; 165 | } 166 | ByteBuffer tmp = ByteBuffer.allocate(4); 167 | try { 168 | fc.position(position); 169 | fc.read(tmp); 170 | } catch(IOException e) { 171 | Panic.panic(e); 172 | } 173 | int size = Parser.parseInt(tmp.array()); 174 | if(position + size + OF_DATA > fileSize) { 175 | return null; 176 | } 177 | 178 | ByteBuffer buf = ByteBuffer.allocate(OF_DATA + size); 179 | try { 180 | fc.position(position); 181 | fc.read(buf); 182 | } catch(IOException e) { 183 | Panic.panic(e); 184 | } 185 | 186 | byte[] log = buf.array(); 187 | int checkSum1 = calChecksum(0, Arrays.copyOfRange(log, OF_DATA, log.length)); 188 | int checkSum2 = Parser.parseInt(Arrays.copyOfRange(log, OF_CHECKSUM, OF_DATA)); 189 | if(checkSum1 != checkSum2) { 190 | return null; 191 | } 192 | position += log.length; 193 | return log; 194 | } 195 | 196 | @Override 197 | public byte[] next() { 198 | lock.lock(); 199 | try { 200 | byte[] log = internNext(); 201 | if(log == null) return null; 202 | return Arrays.copyOfRange(log, OF_DATA, log.length); 203 | } finally { 204 | lock.unlock(); 205 | } 206 | } 207 | 208 | @Override 209 | public void rewind() { 210 | position = 4; 211 | } 212 | 213 | @Override 214 | public void close() { 215 | try { 216 | fc.close(); 217 | file.close(); 218 | } catch(IOException e) { 219 | Panic.panic(e); 220 | } 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to updateObj the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /src/main/resources/static/runtime.b0a62080.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","1","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","window","oldJsonpFunction","slice"],"mappings":"aACE,SAASA,EAAqBC,GAQ7B,IAPA,IAMIC,EAAUC,EANVC,EAAWH,EAAK,GAChBI,EAAcJ,EAAK,GACnBK,EAAiBL,EAAK,GAIHM,EAAI,EAAGC,EAAW,GACpCD,EAAIH,EAASK,OAAQF,IACzBJ,EAAUC,EAASG,GAChBG,OAAOC,UAAUC,eAAeC,KAAKC,EAAiBX,IAAYW,EAAgBX,IACpFK,EAASO,KAAKD,EAAgBX,GAAS,IAExCW,EAAgBX,GAAW,EAE5B,IAAID,KAAYG,EACZK,OAAOC,UAAUC,eAAeC,KAAKR,EAAaH,KACpDc,EAAQd,GAAYG,EAAYH,IAKlC,IAFGe,GAAqBA,EAAoBhB,GAEtCO,EAASC,QACdD,EAASU,OAATV,GAOD,OAHAW,EAAgBJ,KAAKK,MAAMD,EAAiBb,GAAkB,IAGvDe,IAER,SAASA,IAER,IADA,IAAIC,EACIf,EAAI,EAAGA,EAAIY,EAAgBV,OAAQF,IAAK,CAG/C,IAFA,IAAIgB,EAAiBJ,EAAgBZ,GACjCiB,GAAY,EACRC,EAAI,EAAGA,EAAIF,EAAed,OAAQgB,IAAK,CAC9C,IAAIC,EAAQH,EAAeE,GACG,IAA3BX,EAAgBY,KAAcF,GAAY,GAE3CA,IACFL,EAAgBQ,OAAOpB,IAAK,GAC5Be,EAASM,EAAoBA,EAAoBC,EAAIN,EAAe,KAItE,OAAOD,EAIR,IAAIQ,EAAmB,GAKnBhB,EAAkB,CACrBiB,EAAG,GAGAZ,EAAkB,GAGtB,SAASS,EAAoB1B,GAG5B,GAAG4B,EAAiB5B,GACnB,OAAO4B,EAAiB5B,GAAU8B,QAGnC,IAAIC,EAASH,EAAiB5B,GAAY,CACzCK,EAAGL,EACHgC,GAAG,EACHF,QAAS,IAUV,OANAhB,EAAQd,GAAUW,KAAKoB,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAG/DK,EAAOC,GAAI,EAGJD,EAAOD,QAKfJ,EAAoBO,EAAInB,EAGxBY,EAAoBQ,EAAIN,EAGxBF,EAAoBS,EAAI,SAASL,EAASM,EAAMC,GAC3CX,EAAoBY,EAAER,EAASM,IAClC5B,OAAO+B,eAAeT,EAASM,EAAM,CAAEI,YAAY,EAAMC,IAAKJ,KAKhEX,EAAoBgB,EAAI,SAASZ,GACX,oBAAXa,QAA0BA,OAAOC,aAC1CpC,OAAO+B,eAAeT,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DrC,OAAO+B,eAAeT,EAAS,aAAc,CAAEe,OAAO,KAQvDnB,EAAoBoB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQnB,EAAoBmB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKzC,OAAO0C,OAAO,MAGvB,GAFAxB,EAAoBgB,EAAEO,GACtBzC,OAAO+B,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOnB,EAAoBS,EAAEc,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRvB,EAAoB2B,EAAI,SAAStB,GAChC,IAAIM,EAASN,GAAUA,EAAOiB,WAC7B,WAAwB,OAAOjB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAL,EAAoBS,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRX,EAAoBY,EAAI,SAASgB,EAAQC,GAAY,OAAO/C,OAAOC,UAAUC,eAAeC,KAAK2C,EAAQC,IAGzG7B,EAAoB8B,EAAI,GAExB,IAAIC,EAAaC,OAAqB,aAAIA,OAAqB,cAAK,GAChEC,EAAmBF,EAAW5C,KAAKuC,KAAKK,GAC5CA,EAAW5C,KAAOf,EAClB2D,EAAaA,EAAWG,QACxB,IAAI,IAAIvD,EAAI,EAAGA,EAAIoD,EAAWlD,OAAQF,IAAKP,EAAqB2D,EAAWpD,IAC3E,IAAIU,EAAsB4C,EAI1BxC,I","file":"runtime.b0a62080.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t1: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \tvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// run deferred modules from other chunks\n \tcheckDeferredModules();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/VersionManagerImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.locks.Condition; 9 | import java.util.concurrent.locks.Lock; 10 | import java.util.concurrent.locks.ReentrantLock; 11 | 12 | import com.dyx.simpledb.backend.common.AbstractCache; 13 | import com.dyx.simpledb.backend.dm.DataManager; 14 | import com.dyx.simpledb.backend.tbm.Table; 15 | import com.dyx.simpledb.backend.tm.TransactionManager; 16 | import com.dyx.simpledb.backend.tm.TransactionManagerImpl; 17 | import com.dyx.simpledb.backend.utils.Panic; 18 | import com.dyx.simpledb.common.Error; 19 | 20 | public class VersionManagerImpl extends AbstractCache implements VersionManager { 21 | 22 | TransactionManager tm; 23 | DataManager dm; 24 | Map activeTransaction; 25 | Lock lock; 26 | LockTable lt; 27 | private final Lock globalLock = new ReentrantLock(); 28 | 29 | 30 | public VersionManagerImpl(TransactionManager tm, DataManager dm) { 31 | super(0); 32 | this.tm = tm; 33 | this.dm = dm; 34 | this.activeTransaction = new ConcurrentHashMap<>(); 35 | activeTransaction.put(TransactionManagerImpl.SUPER_XID, Transaction.newTransaction(TransactionManagerImpl.SUPER_XID, IsolationLevel.READ_COMMITTED, null)); 36 | this.lock = new ReentrantLock(); 37 | this.lt = new LockTable(); 38 | } 39 | 40 | @Override 41 | public byte[] read(long xid, long uid) throws Exception { 42 | lock.lock(); 43 | Transaction t = activeTransaction.get(xid); 44 | lock.unlock(); 45 | 46 | if (t.err != null) { 47 | throw t.err; 48 | } 49 | 50 | Entry entry = null; 51 | try { 52 | entry = super.get(uid); 53 | } catch (Exception e) { 54 | if (e == Error.NullEntryException) { 55 | return null; 56 | } else { 57 | throw e; 58 | } 59 | } 60 | try { 61 | if (Visibility.isVisible(tm, t, entry)) { 62 | return entry.data(); 63 | } else { 64 | return null; 65 | } 66 | } finally { 67 | entry.release(); 68 | } 69 | } 70 | 71 | @Override 72 | public long insert(long xid, byte[] data) throws Exception { 73 | lock.lock(); 74 | Transaction t = activeTransaction.get(xid); 75 | lock.unlock(); 76 | 77 | if (t.err != null) { 78 | throw t.err; 79 | } 80 | 81 | byte[] raw = Entry.wrapEntryRaw(xid, data); 82 | return dm.insert(xid, raw); 83 | } 84 | 85 | @Override 86 | public void physicalDelete(long xid, Long uid) throws Exception { 87 | lock.lock(); 88 | Transaction t = activeTransaction.get(xid); 89 | lock.unlock(); 90 | 91 | if (t.err != null) { 92 | throw t.err; 93 | } 94 | dm.physicalDelete(uid); 95 | 96 | super.release(uid); 97 | } 98 | 99 | @Override 100 | public Transaction getActiveTransaction(long xid) { 101 | return activeTransaction.get(xid); 102 | } 103 | 104 | @Override 105 | public boolean delete(long xid, long uid) throws Exception { 106 | lock.lock(); 107 | Transaction t = activeTransaction.get(xid); 108 | lock.unlock(); 109 | 110 | if (t.err != null) { 111 | throw t.err; 112 | } 113 | Entry entry = null; 114 | try { 115 | entry = super.get(uid); 116 | } catch (Exception e) { 117 | if (e == Error.NullEntryException) { 118 | return false; 119 | } else { 120 | throw e; 121 | } 122 | } 123 | try { 124 | if (!Visibility.isVisible(tm, t, entry)) { 125 | return false; 126 | } 127 | Lock l = null; 128 | try { 129 | l = lt.add(xid, uid); 130 | } catch (Exception e) { 131 | t.err = Error.ConcurrentUpdateException; 132 | internAbort(xid, true); 133 | t.autoAborted = true; 134 | throw t.err; 135 | } 136 | if (l != null) { 137 | l.lock(); 138 | l.unlock(); 139 | } 140 | 141 | if (entry.getXmax() == xid) { 142 | return false; 143 | } 144 | 145 | if (Visibility.isVersionSkip(tm, t, entry)) { 146 | t.err = Error.ConcurrentUpdateException; 147 | internAbort(xid, true); 148 | t.autoAborted = true; 149 | throw t.err; 150 | } 151 | 152 | entry.setXmax(xid); 153 | return true; 154 | 155 | } finally { 156 | entry.release(); 157 | } 158 | } 159 | 160 | @Override 161 | public long begin(IsolationLevel isolationLevel) { 162 | globalLock.lock(); // 获取全局锁 163 | lock.lock(); 164 | try { 165 | if (isolationLevel != IsolationLevel.SERIALIZABLE) { 166 | globalLock.unlock(); // 解除非全局锁 167 | } 168 | long xid = tm.begin(); 169 | Transaction t = Transaction.newTransaction( 170 | xid, isolationLevel == null ? IsolationLevel.READ_COMMITTED : isolationLevel, activeTransaction); 171 | activeTransaction.put(xid, t); 172 | 173 | return xid; 174 | } finally { 175 | lock.unlock(); 176 | } 177 | } 178 | 179 | @Override 180 | public void commit(long xid) throws Exception { 181 | lock.lock(); 182 | Transaction t = activeTransaction.get(xid); 183 | lock.unlock(); 184 | 185 | try { 186 | if (t.err != null) { 187 | throw t.err; 188 | } 189 | } catch (NullPointerException n) { 190 | System.out.println(xid); 191 | System.out.println(activeTransaction.keySet()); 192 | Panic.panic(n); 193 | } 194 | 195 | lock.lock(); 196 | activeTransaction.remove(xid); 197 | lock.unlock(); 198 | 199 | lt.remove(xid); 200 | tm.commit(xid); 201 | 202 | // 通知所有关联的表进行索引提交 203 | for (Table table : t.getModifiedTables()) { 204 | table.commit(xid); 205 | } 206 | 207 | if (t.isolationLevel == IsolationLevel.SERIALIZABLE && globalLock.tryLock()) { 208 | globalLock.unlock(); // 释放全局锁 209 | } 210 | } 211 | 212 | @Override 213 | public void abort(long xid) { 214 | internAbort(xid, false); 215 | } 216 | 217 | private void internAbort(long xid, boolean autoAborted) { 218 | lock.lock(); 219 | Transaction t = activeTransaction.get(xid); 220 | if (!autoAborted) { 221 | activeTransaction.remove(xid); 222 | } 223 | lock.unlock(); 224 | 225 | if (t.autoAborted){ 226 | if (t.isolationLevel == IsolationLevel.SERIALIZABLE) globalLock.unlock(); // 释放全局锁 227 | return; 228 | } 229 | lt.remove(xid); 230 | tm.abort(xid); 231 | 232 | // 通知所有关联的表进行索引提交 233 | for (Table table : t.getModifiedTables()) { 234 | table.rollback(xid); 235 | } 236 | 237 | if (t.isolationLevel == IsolationLevel.SERIALIZABLE) globalLock.unlock(); // 释放全局锁 238 | 239 | } 240 | 241 | public void releaseEntry(Entry entry) { 242 | super.release(entry.getUid()); 243 | } 244 | 245 | @Override 246 | protected Entry getForCache(long uid) throws Exception { 247 | Entry entry = Entry.loadEntry(this, uid); 248 | if (entry == null) { 249 | throw Error.NullEntryException; 250 | } 251 | return entry; 252 | } 253 | 254 | @Override 255 | protected void releaseForCache(Entry entry) { 256 | entry.remove(); 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/vm/LockTable.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.vm; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.locks.Lock; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | import com.dyx.simpledb.common.Error; 8 | 9 | public class LockTable { 10 | 11 | private Map> x2u; // 某个XID已获得的资源UID列表 12 | private Map u2x; // UID被某个XID持有 13 | private Map> wait; // 正在等待UID的XID列表 14 | private Map waitLock; // 正在等待资源的XID的锁 15 | private Map waitU; // XID正在等待的UID 16 | private Lock lock; 17 | private Map waitStartTime; // 记录每个XID进入等待状态的时间 18 | private static final int CHECK_INTERVAL_MS = 1000; // 检查间隔(0.6秒) 19 | private static final int TIMEOUT_THRESHOLD_MS = 30000; // 超时时间阈值(30秒) 20 | 21 | 22 | public LockTable() { 23 | x2u = new HashMap<>(); 24 | u2x = new HashMap<>(); 25 | wait = new HashMap<>(); 26 | waitLock = new HashMap<>(); 27 | waitU = new HashMap<>(); 28 | lock = new ReentrantLock(); 29 | waitStartTime = new HashMap<>(); 30 | startTimeoutDeadlockChecker(); 31 | } 32 | 33 | // 尝试获取资源,如需等待则检测死锁并处理 34 | public Lock add(long xid, long uid) throws Exception { 35 | lock.lock(); 36 | try { 37 | if (isInList(x2u, xid, uid)) { 38 | return null; // 已拥有资源 39 | } 40 | if (!u2x.containsKey(uid)) { // 资源未被占用 41 | u2x.put(uid, xid); 42 | putIntoList(x2u, xid, uid); 43 | return null; 44 | } 45 | waitU.put(xid, uid); // 资源被占用,进入等待 46 | putIntoList(wait, uid, xid); 47 | waitStartTime.put(xid, System.currentTimeMillis()); 48 | 49 | // 仅在等待链长度达到阈值时才触发死锁检测 50 | if (hasDeadLock()) { 51 | waitU.remove(xid); 52 | removeFromList(wait, uid, xid); 53 | throw Error.DeadlockException; 54 | } 55 | 56 | Lock l = new ReentrantLock(); 57 | l.lock(); 58 | waitLock.put(xid, l); 59 | return l; 60 | 61 | } finally { 62 | lock.unlock(); 63 | } 64 | } 65 | 66 | public void remove(long xid) { 67 | lock.lock(); 68 | try { 69 | List l = x2u.get(xid); 70 | if (l != null) { 71 | while (l.size() > 0) { 72 | Long uid = l.remove(0); 73 | selectNewXID(uid); // 重新分配资源 74 | } 75 | } 76 | waitU.remove(xid); 77 | x2u.remove(xid); 78 | waitLock.remove(xid); 79 | } finally { 80 | lock.unlock(); 81 | } 82 | } 83 | 84 | // 从等待队列中选择一个xid来占用uid 85 | private void selectNewXID(long uid) { 86 | u2x.remove(uid); 87 | List l = wait.get(uid); 88 | if (l == null) return; 89 | assert l.size() > 0; 90 | 91 | while (l.size() > 0) { 92 | long xid = l.remove(0); 93 | if (!waitLock.containsKey(xid)) { 94 | continue; 95 | } else { 96 | u2x.put(uid, xid); 97 | Lock lo = waitLock.remove(xid); 98 | waitU.remove(xid); 99 | lo.unlock(); // 解锁等待事务 100 | break; 101 | } 102 | } 103 | 104 | if (l.size() == 0) wait.remove(uid); 105 | } 106 | 107 | private Map xidStamp; 108 | private Map pathCache; // 路径缓存 109 | private int stamp; 110 | 111 | // 死锁检测,使用DFS 112 | private boolean hasDeadLock() { 113 | xidStamp = new HashMap<>(); 114 | pathCache = new HashMap<>(); 115 | stamp = 1; 116 | for (long xid : x2u.keySet()) { 117 | if (xidStamp.getOrDefault(xid, 0) > 0) continue; 118 | stamp++; 119 | if (dfs(xid)) return true; 120 | } 121 | return false; 122 | } 123 | 124 | private boolean dfs(long xid) { 125 | // 如果路径缓存中已经有结果,直接返回缓存的值 126 | if (pathCache.containsKey(xid)) { 127 | return pathCache.get(xid); 128 | } 129 | 130 | Integer stp = xidStamp.get(xid); 131 | if (stp != null && stp == stamp) { 132 | pathCache.put(xid, true); // 更新路径缓存 133 | return true; 134 | } 135 | if (stp != null && stp < stamp) { 136 | pathCache.put(xid, false); // 更新路径缓存 137 | return false; 138 | } 139 | xidStamp.put(xid, stamp); 140 | 141 | Long uid = waitU.get(xid); 142 | if (uid == null) { 143 | pathCache.put(xid, false); // 更新路径缓存 144 | return false; 145 | } 146 | Long x = u2x.get(uid); 147 | boolean hasCycle = x != null && dfs(x); 148 | pathCache.put(xid, hasCycle); // 更新路径缓存 149 | return hasCycle; 150 | } 151 | 152 | 153 | private void removeFromList(Map> listMap, long uid0, long uid1) { 154 | List l = listMap.get(uid0); 155 | if (l == null) return; 156 | Iterator i = l.iterator(); 157 | while (i.hasNext()) { 158 | long e = i.next(); 159 | if (e == uid1) { 160 | i.remove(); 161 | break; 162 | } 163 | } 164 | if (l.size() == 0) { 165 | listMap.remove(uid0); 166 | } 167 | } 168 | 169 | private void putIntoList(Map> listMap, long uid0, long uid1) { 170 | if (!listMap.containsKey(uid0)) { 171 | listMap.put(uid0, new ArrayList<>()); 172 | } 173 | listMap.get(uid0).add(0, uid1); 174 | } 175 | 176 | private boolean isInList(Map> listMap, long uid0, long uid1) { 177 | List l = listMap.get(uid0); 178 | if (l == null) return false; 179 | Iterator i = l.iterator(); 180 | while (i.hasNext()) { 181 | long e = i.next(); 182 | if (e == uid1) { 183 | return true; 184 | } 185 | } 186 | return false; 187 | } 188 | 189 | private void startTimeoutDeadlockChecker() { 190 | new Thread(() -> { 191 | while (true) { 192 | try { 193 | Thread.sleep(CHECK_INTERVAL_MS); 194 | checkForTimeouts(TIMEOUT_THRESHOLD_MS); 195 | } catch (InterruptedException e) { 196 | throw new RuntimeException(e); 197 | } 198 | } 199 | }).start(); 200 | } 201 | 202 | public void checkForTimeouts(long timeout) { 203 | lock.lock(); 204 | try { 205 | long currentTime = System.currentTimeMillis(); 206 | Iterator> iterator = waitStartTime.entrySet().iterator(); 207 | while (iterator.hasNext()) { 208 | Map.Entry entry = iterator.next(); 209 | long xid = entry.getKey(); 210 | long startTime = entry.getValue(); 211 | if (currentTime - startTime >= timeout) { 212 | // 超时,执行回滚操作 213 | rollbackTimeoutTransaction(xid); 214 | iterator.remove(); // 从等待时间记录中移除 215 | } 216 | } 217 | } finally { 218 | lock.unlock(); 219 | } 220 | } 221 | 222 | private void rollbackTimeoutTransaction(long xid) { 223 | System.out.println("Transaction " + xid + " has timed out and will be rolled back."); 224 | 225 | // 解除事务等待的资源 226 | Long uid = waitU.remove(xid); 227 | if (uid != null) { 228 | removeFromList(wait, uid, xid); 229 | } 230 | // 释放所有已占用资源 231 | List resources = x2u.remove(xid); 232 | if (resources != null) { 233 | for (Long resource : resources) { 234 | selectNewXID(resource); 235 | } 236 | } 237 | // 通知等待该事务的其他线程 238 | Lock l = waitLock.remove(xid); 239 | if (l != null && ((ReentrantLock) l).isHeldByCurrentThread()) { 240 | l.unlock(); 241 | } 242 | } 243 | 244 | 245 | } 246 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/dm/Recover.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.dm; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | 10 | import com.google.common.primitives.Bytes; 11 | 12 | import com.dyx.simpledb.backend.common.SubArray; 13 | import com.dyx.simpledb.backend.dm.dataItem.DataItem; 14 | import com.dyx.simpledb.backend.dm.logger.Logger; 15 | import com.dyx.simpledb.backend.dm.page.Page; 16 | import com.dyx.simpledb.backend.dm.page.PageX; 17 | import com.dyx.simpledb.backend.dm.pageCache.PageCache; 18 | import com.dyx.simpledb.backend.tm.TransactionManager; 19 | import com.dyx.simpledb.backend.utils.Panic; 20 | import com.dyx.simpledb.backend.utils.Parser; 21 | 22 | public class Recover { 23 | 24 | private static final byte LOG_TYPE_INSERT = 0; 25 | private static final byte LOG_TYPE_UPDATE = 1; 26 | 27 | private static final int REDO = 0; 28 | private static final int UNDO = 1; 29 | 30 | static class InsertLogInfo { 31 | long xid; 32 | int pgno; 33 | short offset; 34 | byte[] raw; 35 | } 36 | 37 | static class UpdateLogInfo { 38 | long xid; 39 | int pgno; 40 | short offset; 41 | byte[] oldRaw; 42 | byte[] newRaw; 43 | } 44 | 45 | public static void recover(TransactionManager tm, Logger lg, PageCache pc) { 46 | System.out.println("Recovering..."); 47 | 48 | lg.rewind(); 49 | int maxPgno = 0; 50 | while(true) { 51 | byte[] log = lg.next(); 52 | if(log == null) break; 53 | int pgno; 54 | if(isInsertLog(log)) { 55 | InsertLogInfo li = parseInsertLog(log); 56 | pgno = li.pgno; 57 | } else { 58 | UpdateLogInfo li = parseUpdateLog(log); 59 | pgno = li.pgno; 60 | } 61 | if(pgno > maxPgno) { 62 | maxPgno = pgno; 63 | } 64 | } 65 | if(maxPgno == 0) { 66 | maxPgno = 1; 67 | } 68 | pc.truncateByBgno(maxPgno); 69 | System.out.println("Truncate to " + maxPgno + " pages."); 70 | 71 | redoTranscations(tm, lg, pc); 72 | System.out.println("Redo Transactions Over."); 73 | 74 | undoTranscations(tm, lg, pc); 75 | System.out.println("Undo Transactions Over."); 76 | 77 | System.out.println("Recovery Over."); 78 | } 79 | 80 | private static void redoTranscations(TransactionManager tm, Logger lg, PageCache pc) { 81 | lg.rewind(); 82 | while(true) { 83 | byte[] log = lg.next(); 84 | if(log == null) break; 85 | if(isInsertLog(log)) { 86 | InsertLogInfo li = parseInsertLog(log); 87 | long xid = li.xid; 88 | if(!tm.isActive(xid)) { 89 | doInsertLog(pc, log, REDO); 90 | } 91 | } else { 92 | UpdateLogInfo xi = parseUpdateLog(log); 93 | long xid = xi.xid; 94 | if(!tm.isActive(xid)) { 95 | doUpdateLog(pc, log, REDO); 96 | } 97 | } 98 | } 99 | } 100 | 101 | private static void undoTranscations(TransactionManager tm, Logger lg, PageCache pc) { 102 | Map> logCache = new HashMap<>(); 103 | lg.rewind(); 104 | while(true) { 105 | byte[] log = lg.next(); 106 | if(log == null) break; 107 | if(isInsertLog(log)) { 108 | InsertLogInfo li = parseInsertLog(log); 109 | long xid = li.xid; 110 | if(tm.isActive(xid)) { 111 | if(!logCache.containsKey(xid)) { 112 | logCache.put(xid, new ArrayList<>()); 113 | } 114 | logCache.get(xid).add(log); 115 | } 116 | } else { 117 | UpdateLogInfo xi = parseUpdateLog(log); 118 | long xid = xi.xid; 119 | if(tm.isActive(xid)) { 120 | if(!logCache.containsKey(xid)) { 121 | logCache.put(xid, new ArrayList<>()); 122 | } 123 | logCache.get(xid).add(log); 124 | } 125 | } 126 | } 127 | 128 | // 对所有active log进行倒序undo 129 | for(Entry> entry : logCache.entrySet()) { 130 | List logs = entry.getValue(); 131 | for (int i = logs.size()-1; i >= 0; i --) { 132 | byte[] log = logs.get(i); 133 | if(isInsertLog(log)) { 134 | doInsertLog(pc, log, UNDO); 135 | } else { 136 | doUpdateLog(pc, log, UNDO); 137 | } 138 | } 139 | tm.abort(entry.getKey()); 140 | } 141 | } 142 | 143 | private static boolean isInsertLog(byte[] log) { 144 | return log[0] == LOG_TYPE_INSERT; 145 | } 146 | 147 | // [LogType] [XID] [UID] [OldRaw] [NewRaw] 148 | private static final int OF_TYPE = 0; 149 | private static final int OF_XID = OF_TYPE+1; 150 | private static final int OF_UPDATE_UID = OF_XID+8; 151 | private static final int OF_UPDATE_RAW = OF_UPDATE_UID+8; 152 | 153 | public static byte[] updateLog(long xid, DataItem di) { 154 | byte[] logType = {LOG_TYPE_UPDATE}; 155 | byte[] xidRaw = Parser.long2Byte(xid); 156 | byte[] uidRaw = Parser.long2Byte(di.getUid()); 157 | byte[] oldRaw = di.getOldRaw(); 158 | SubArray raw = di.getRaw(); 159 | byte[] newRaw = Arrays.copyOfRange(raw.raw, raw.start, raw.end); 160 | return Bytes.concat(logType, xidRaw, uidRaw, oldRaw, newRaw); 161 | } 162 | 163 | private static UpdateLogInfo parseUpdateLog(byte[] log) { 164 | UpdateLogInfo li = new UpdateLogInfo(); 165 | li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_UPDATE_UID)); 166 | long uid = Parser.parseLong(Arrays.copyOfRange(log, OF_UPDATE_UID, OF_UPDATE_RAW)); 167 | li.offset = (short)(uid & ((1L << 16) - 1)); 168 | uid >>>= 32; 169 | li.pgno = (int)(uid & ((1L << 32) - 1)); 170 | int length = (log.length - OF_UPDATE_RAW) / 2; 171 | li.oldRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW, OF_UPDATE_RAW+length); 172 | li.newRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW+length, OF_UPDATE_RAW+length*2); 173 | return li; 174 | } 175 | 176 | private static void doUpdateLog(PageCache pc, byte[] log, int flag) { 177 | int pgno; 178 | short offset; 179 | byte[] raw; 180 | if(flag == REDO) { 181 | UpdateLogInfo xi = parseUpdateLog(log); 182 | pgno = xi.pgno; 183 | offset = xi.offset; 184 | raw = xi.newRaw; 185 | } else { 186 | UpdateLogInfo xi = parseUpdateLog(log); 187 | pgno = xi.pgno; 188 | offset = xi.offset; 189 | raw = xi.oldRaw; 190 | } 191 | Page pg = null; 192 | try { 193 | pg = pc.getPage(pgno); 194 | } catch (Exception e) { 195 | Panic.panic(e); 196 | } 197 | try { 198 | PageX.recoverUpdate(pg, raw, offset); 199 | } finally { 200 | pg.release(); 201 | } 202 | } 203 | 204 | // [LogType] [XID] [Pgno] [Offset] [Raw] 205 | private static final int OF_INSERT_PGNO = OF_XID+8; 206 | private static final int OF_INSERT_OFFSET = OF_INSERT_PGNO+4; 207 | private static final int OF_INSERT_RAW = OF_INSERT_OFFSET+2; 208 | 209 | public static byte[] insertLog(long xid, Page pg, byte[] raw) { 210 | byte[] logTypeRaw = {LOG_TYPE_INSERT}; 211 | byte[] xidRaw = Parser.long2Byte(xid); 212 | byte[] pgnoRaw = Parser.int2Byte(pg.getPageNumber()); 213 | byte[] offsetRaw = Parser.short2Byte(PageX.getFSO(pg)); 214 | return Bytes.concat(logTypeRaw, xidRaw, pgnoRaw, offsetRaw, raw); 215 | } 216 | 217 | private static InsertLogInfo parseInsertLog(byte[] log) { 218 | InsertLogInfo li = new InsertLogInfo(); 219 | li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_INSERT_PGNO)); 220 | li.pgno = Parser.parseInt(Arrays.copyOfRange(log, OF_INSERT_PGNO, OF_INSERT_OFFSET)); 221 | li.offset = Parser.parseShort(Arrays.copyOfRange(log, OF_INSERT_OFFSET, OF_INSERT_RAW)); 222 | li.raw = Arrays.copyOfRange(log, OF_INSERT_RAW, log.length); 223 | return li; 224 | } 225 | 226 | private static void doInsertLog(PageCache pc, byte[] log, int flag) { 227 | InsertLogInfo li = parseInsertLog(log); 228 | Page pg = null; 229 | try { 230 | pg = pc.getPage(li.pgno); 231 | } catch(Exception e) { 232 | Panic.panic(e); 233 | } 234 | try { 235 | if(flag == UNDO) { 236 | DataItem.setDataItemRawInvalid(li.raw); 237 | } 238 | PageX.recoverInsert(pg, li.raw, li.offset); 239 | } finally { 240 | pg.release(); 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/backend/tbm/TableManagerImpl.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.backend.tbm; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.locks.Lock; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | import com.dyx.simpledb.backend.dm.DataManager; 8 | import com.dyx.simpledb.backend.parser.statement.*; 9 | import com.dyx.simpledb.backend.parser.statement.DeleteObj; 10 | import com.dyx.simpledb.backend.tm.TransactionManagerImpl; 11 | import com.dyx.simpledb.backend.utils.Parser; 12 | import com.dyx.simpledb.backend.utils.PrintUtil; 13 | import com.dyx.simpledb.backend.vm.IsolationLevel; 14 | import com.dyx.simpledb.backend.vm.VersionManager; 15 | import com.dyx.simpledb.common.Error; 16 | 17 | public class TableManagerImpl implements TableManager { 18 | VersionManager vm; 19 | DataManager dm; 20 | private Booter booter; 21 | private Map tableCache; 22 | private Map> xidTableCache; 23 | private Lock lock; 24 | private Set prohibitTables; 25 | 26 | TableManagerImpl(VersionManager vm, DataManager dm, Booter booter) { 27 | this.vm = vm; 28 | this.dm = dm; 29 | this.booter = booter; 30 | this.tableCache = new HashMap<>(); 31 | this.xidTableCache = new HashMap<>(); 32 | lock = new ReentrantLock(); 33 | this.prohibitTables = new HashSet<>(); 34 | Collections.addAll(this.prohibitTables, 35 | "select", "insert", "update", "delete", "create", 36 | "drop", "alter", "from", "where", "group", "order", "join", 37 | "union", "null", "index", "table", "column", "database" 38 | ); 39 | loadTables(); 40 | } 41 | 42 | private void loadTables() { 43 | long uid = firstTableUid(); 44 | while (uid != 0) { 45 | Table tb = Table.loadTable(this, uid); 46 | uid = tb.nextUid; 47 | tableCache.put(tb.name, tb); 48 | } 49 | } 50 | 51 | private long firstTableUid() { 52 | byte[] raw = booter.load(); 53 | return Parser.parseLong(raw); 54 | } 55 | 56 | private void updateFirstTableUid(long uid) { 57 | byte[] raw = Parser.long2Byte(uid); 58 | booter.update(raw); 59 | } 60 | 61 | @Override 62 | public BeginRes begin(Begin begin) { 63 | BeginRes res = new BeginRes(); 64 | IsolationLevel isolationLevel = begin.isolationLevel; 65 | res.xid = vm.begin(isolationLevel); 66 | res.result = "begin".getBytes(); 67 | return res; 68 | } 69 | 70 | @Override 71 | public byte[] commit(long xid) throws Exception { 72 | vm.commit(xid); 73 | return "commit".getBytes(); 74 | } 75 | 76 | @Override 77 | public byte[] abort(long xid) { 78 | vm.abort(xid); 79 | return "abort".getBytes(); 80 | } 81 | 82 | @Override 83 | public byte[] show(long xid, Show stat) { 84 | lock.lock(); 85 | try { 86 | List> entries = new ArrayList<>(); 87 | String[] columns = null; 88 | Map columnData = null; 89 | 90 | if (stat.isTable) { 91 | columns = Arrays.asList("tables").toArray(new String[0]); 92 | for (String tableName : tableCache.keySet()) { 93 | columnData = new HashMap<>(); 94 | columnData.put("tables", tableName); 95 | entries.add(columnData); 96 | } 97 | return PrintUtil.printTable(columns, entries).getBytes(); 98 | } 99 | if (tableCache.containsKey(stat.tableName)) { 100 | columns = Arrays.asList("field", "fieldType", "isIndexed", "constraint").toArray(new String[0]); 101 | Table table = tableCache.get(stat.tableName); 102 | for (Field field : table.fields) { 103 | columnData = new HashMap<>(); 104 | columnData.put("field", field.fieldName); 105 | columnData.put("fieldType", field.fieldType); 106 | columnData.put("isIndexed", field.isIndexed() ? "Index" : "NoIndex"); 107 | columnData.put("constraint", field.printConstraint()); 108 | entries.add(columnData); 109 | } 110 | return PrintUtil.printTable(columns, entries).getBytes(); 111 | } else { 112 | return "Table not found!".getBytes(); 113 | } 114 | } finally { 115 | lock.unlock(); 116 | } 117 | 118 | } 119 | 120 | @Override 121 | public byte[] create(long xid, Create create) throws Exception { 122 | lock.lock(); 123 | try { 124 | if (prohibitTables.contains(create.tableName)){ 125 | throw Error.TableNotCreateException; 126 | } 127 | if (tableCache.containsKey(create.tableName)) { 128 | throw Error.DuplicatedTableException; 129 | } 130 | Table table = Table.createTable(this, firstTableUid(), xid, create); 131 | updateFirstTableUid(table.uid); 132 | tableCache.put(create.tableName, table); 133 | if (!xidTableCache.containsKey(xid)) { 134 | xidTableCache.put(xid, new ArrayList<>()); 135 | } 136 | xidTableCache.get(xid).add(table); 137 | return ("create " + create.tableName).getBytes(); 138 | } finally { 139 | lock.unlock(); 140 | } 141 | } 142 | 143 | @Override 144 | public byte[] insert(long xid, InsertObj insertObj) throws Exception { 145 | lock.lock(); 146 | Table table = tableCache.get(insertObj.tableName); 147 | lock.unlock(); 148 | if (table == null) { 149 | throw Error.TableNotFoundException; 150 | } 151 | table.insert(xid, insertObj); 152 | return "insert".getBytes(); 153 | } 154 | 155 | @Override 156 | public byte[] read(long xid, SelectObj read) throws Exception { 157 | lock.lock(); 158 | Table table = tableCache.get(read.tableName); 159 | lock.unlock(); 160 | if (table == null) { 161 | throw Error.TableNotFoundException; 162 | } 163 | return table.read(xid, read).getBytes(); 164 | } 165 | 166 | @Override 167 | public byte[] update(long xid, UpdateObj updateObj) throws Exception { 168 | lock.lock(); 169 | Table table = tableCache.get(updateObj.tableName); 170 | lock.unlock(); 171 | if (table == null) { 172 | throw Error.TableNotFoundException; 173 | } 174 | int count = table.update(xid, updateObj); 175 | return ("update " + count).getBytes(); 176 | } 177 | 178 | @Override 179 | public byte[] delete(long xid, DeleteObj deleteObj) throws Exception { 180 | lock.lock(); 181 | Table table = tableCache.get(deleteObj.tableName); 182 | lock.unlock(); 183 | if (table == null) { 184 | throw Error.TableNotFoundException; 185 | } 186 | int count = table.delete(xid, deleteObj); 187 | return ("delete " + count).getBytes(); 188 | } 189 | 190 | @Override 191 | public byte[] drop(long xid, DropObj stat) throws Exception { 192 | lock.lock(); 193 | Table table = tableCache.get(stat.tableName); 194 | if (table == null) { 195 | lock.unlock(); 196 | throw Error.TableNotFoundException; 197 | } 198 | 199 | try { 200 | // 执行表的删除操作 201 | table.drop(xid); 202 | // 从 `tableCache` 中移除表 203 | tableCache.remove(stat.tableName); 204 | // 更新表链中的 `nextUid` 205 | updateTableChainAfterDrop(table.uid); 206 | 207 | return ("drop " + stat.tableName).getBytes(); 208 | } finally { 209 | lock.unlock(); 210 | } 211 | } 212 | 213 | private void updateTableChainAfterDrop(long droppedTableUid) throws Exception { 214 | long firstUid = firstTableUid(); 215 | 216 | if (firstUid == droppedTableUid) { 217 | // 如果删除的是第一个表,更新 Booter 中的 UID 218 | long nextUid = tableCache.values().stream() 219 | .filter(t -> t.uid != droppedTableUid) 220 | .map(t -> t.uid) 221 | .findFirst() 222 | .orElse(0L); 223 | updateFirstTableUid(nextUid); 224 | } else { 225 | // 如果删除的不是第一个表,更新前一个表的 nextUid 226 | Table previousTable = null; 227 | for (Table table : tableCache.values()) { 228 | if (table.nextUid == droppedTableUid) { 229 | previousTable = table; 230 | break; 231 | } 232 | } 233 | 234 | if (previousTable != null) { 235 | long nextUid = tableCache.values().stream() 236 | .filter(t -> t.uid != droppedTableUid) 237 | .filter(t -> t.uid > droppedTableUid) // 再次使用filter方法过滤出UID大于droppedTableUid的表 238 | .map(t -> t.uid) 239 | .findFirst() 240 | .orElse(0L); 241 | previousTable.nextUid = nextUid; 242 | previousTable.persistSelf(TransactionManagerImpl.SUPER_XID); // 保存更改 243 | } 244 | } 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/com/dyx/simpledb/websocket/TerminalWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.dyx.simpledb.websocket; 2 | 3 | import cn.hutool.json.JSONObject; 4 | import com.dyx.simpledb.backend.dm.DataManager; 5 | import com.dyx.simpledb.backend.server.Executor; 6 | import com.dyx.simpledb.backend.tbm.TableManager; 7 | import com.dyx.simpledb.backend.tm.TransactionManager; 8 | import com.dyx.simpledb.backend.vm.VersionManager; 9 | import com.dyx.simpledb.backend.vm.VersionManagerImpl; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.socket.CloseStatus; 15 | import org.springframework.web.socket.TextMessage; 16 | import org.springframework.web.socket.WebSocketSession; 17 | import org.springframework.web.socket.handler.TextWebSocketHandler; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.util.Map; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | 26 | import static com.dyx.simpledb.backend.Launcher.DEFALUT_MEM; 27 | 28 | @Slf4j 29 | @Component 30 | public class TerminalWebSocketHandler extends TextWebSocketHandler { 31 | 32 | @Autowired 33 | private UserManager userManager; 34 | 35 | @Value("${custom.db.path}") 36 | private String dbPath; 37 | private final Map sessionExecutorMap = new ConcurrentHashMap<>(); 38 | 39 | @Override 40 | public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { 41 | String payload = message.getPayload(); 42 | JSONObject jsonObject = new JSONObject(payload); 43 | String sql = jsonObject.getStr("command"); 44 | String clientIp = (String) session.getAttributes().get("clientIp"); 45 | String sessionId = session.getId(); // 每个 WebSocket 会话的唯一标识符 46 | 47 | log.info("User with IP: {}, session ID: {} executed SQL: {}", clientIp, sessionId, sql); 48 | // 获取当前 WebSocket 连接的专属线程池 49 | ExecutorService executorService = sessionExecutorMap.computeIfAbsent(sessionId, key -> Executors.newSingleThreadExecutor()); 50 | 51 | executorService.submit(() -> { 52 | try { 53 | if ("init".equalsIgnoreCase(sql.trim()) || "init;".equalsIgnoreCase(sql.trim())) { 54 | handleInitCommand(session, clientIp, sessionId); 55 | } else { 56 | UserSession userSession = userManager.getUserSession(clientIp); 57 | if (userSession == null || userSession.getExecutor(sessionId) == null) { 58 | session.sendMessage(new TextMessage(createMessage("Please init database", "error"))); 59 | } else { 60 | userSession.updateLastAccessedTime(); // 更新最后访问时间 61 | handleSqlCommand(session, userSession, sessionId, sql); 62 | } 63 | } 64 | } catch (IOException e) { 65 | e.printStackTrace(); 66 | } 67 | }); 68 | } 69 | 70 | private void handleInitCommand(WebSocketSession session, String clientIp, String sessionId) throws IOException { 71 | UserSession userSession = userManager.getUserSession(clientIp); 72 | if (userSession != null && userSession.getExecutor(sessionId) != null) { 73 | session.sendMessage(new TextMessage(createMessage("Database is already initialized in this session.", "success"))); 74 | return; 75 | } 76 | 77 | if (userSession == null) { 78 | userSession = new UserSession(clientIp, System.currentTimeMillis()); 79 | userManager.addUserSession(clientIp, userSession); 80 | } 81 | 82 | String directoryPath = dbPath + File.separator + clientIp; 83 | String dbFilePath = directoryPath + File.separator + clientIp; 84 | 85 | File ipDirectory = new File(directoryPath); 86 | if (!ipDirectory.exists() && !ipDirectory.mkdirs()) { 87 | session.sendMessage(new TextMessage(createMessage("Database init failed: cannot create directory.", "error"))); 88 | return; 89 | } 90 | boolean databaseExists = checkIfDatabaseFilesExist(directoryPath, clientIp); 91 | 92 | if (databaseExists) { 93 | initializeDatabase(userSession, dbFilePath, sessionId); 94 | } else { 95 | createDatabase(dbFilePath); 96 | initializeDatabase(userSession, dbFilePath, sessionId); 97 | } 98 | 99 | session.sendMessage(new TextMessage(createMessage("Database init and load success!", "success"))); 100 | } 101 | 102 | private boolean checkIfDatabaseFilesExist(String directoryPath, String filePrefix) { 103 | String[] requiredFiles = {".bt", ".db", ".log", ".xid"}; 104 | 105 | File dir = new File(directoryPath); 106 | if (dir.exists() && dir.isDirectory()) { 107 | for (String suffix : requiredFiles) { 108 | File file = new File(directoryPath, filePrefix + suffix); 109 | if (!file.exists()) { 110 | return false; 111 | } 112 | } 113 | return true; 114 | } 115 | return false; 116 | } 117 | 118 | private void createDatabase(String path) { 119 | TransactionManager tm = TransactionManager.create(path); 120 | DataManager dm = DataManager.create(path, DEFALUT_MEM, tm); 121 | VersionManager vm = new VersionManagerImpl(tm, dm); 122 | TableManager.create(path, vm, dm); 123 | tm.close(); 124 | dm.close(); 125 | } 126 | 127 | private void initializeDatabase(UserSession userSession, String dbFilePath, String sessionId) { 128 | if (userSession.getTableManager() == null) { 129 | TransactionManager tm = TransactionManager.open(dbFilePath); 130 | DataManager dm = DataManager.open(dbFilePath, 32 * 1024 * 1024, tm); 131 | VersionManager vm = new VersionManagerImpl(tm, dm); 132 | TableManager tbm = TableManager.open(dbFilePath, vm, dm); 133 | 134 | userSession.setTransactionManager(tm); 135 | userSession.setDataManager(dm); 136 | userSession.setTableManager(tbm); 137 | } 138 | 139 | Executor executor = new Executor(userSession.getTableManager()); 140 | userSession.setExecutor(sessionId, executor); 141 | } 142 | 143 | private void handleSqlCommand(WebSocketSession session, UserSession userSession, String sessionId, String sql) throws IOException { 144 | try { 145 | Executor executor = userSession.getExecutor(sessionId); 146 | byte[] execute = executor.execute(sql.getBytes()); 147 | String result = new String(execute).trim(); 148 | session.sendMessage(new TextMessage(createMessage(result, "success"))); 149 | } catch (NullPointerException e) { 150 | e.printStackTrace(); 151 | session.sendMessage(new TextMessage(createMessage("System error, please contact the administrator", "error"))); 152 | }catch (Exception e) { 153 | e.printStackTrace(); 154 | session.sendMessage(new TextMessage(createMessage(e.getMessage(), "error"))); 155 | } 156 | } 157 | 158 | private String createMessage(String data, String type) { 159 | return "{\"type\": \"" + type + "\", \"data\": \"" + escapeJson(data) + "\"}"; 160 | } 161 | 162 | private String escapeJson(String data) { 163 | if (data == null) { 164 | return ""; 165 | } 166 | StringBuilder escaped = new StringBuilder(); 167 | for (char c : data.toCharArray()) { 168 | switch (c) { 169 | case '\"': 170 | escaped.append("\\\""); 171 | break; 172 | case '\\': 173 | escaped.append("\\\\"); 174 | break; 175 | case '\b': 176 | escaped.append("\\b"); 177 | break; 178 | case '\f': 179 | escaped.append("\\f"); 180 | break; 181 | case '\n': 182 | escaped.append("\\n"); 183 | break; 184 | case '\r': 185 | escaped.append("\\r"); 186 | break; 187 | case '\t': 188 | escaped.append("\\t"); 189 | break; 190 | default: 191 | if (c <= 0x1F) { 192 | escaped.append(String.format("\\u%04x", (int) c)); 193 | } else { 194 | escaped.append(c); 195 | } 196 | } 197 | } 198 | return escaped.toString(); 199 | } 200 | 201 | @Override 202 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 203 | String clientIp = (String) session.getAttributes().get("clientIp"); 204 | String sessionId = session.getId(); 205 | 206 | UserSession userSession = userManager.getUserSession(clientIp); 207 | if (userSession != null) { 208 | userSession.removeExecutor(sessionId); 209 | userSession.removeSession(sessionId); 210 | 211 | ExecutorService executorService = sessionExecutorMap.remove(sessionId); 212 | if (executorService != null) { 213 | executorService.shutdown(); 214 | } 215 | 216 | if (!userSession.hasActiveSessions()) { 217 | userSession.updateLastAccessedTime(); 218 | } 219 | } 220 | 221 | super.afterConnectionClosed(session, status); 222 | } 223 | } 224 | --------------------------------------------------------------------------------