├── skiplist
├── .idea
│ ├── vcs.xml
│ ├── encodings.xml
│ ├── misc.xml
│ └── workspace.xml
├── .gitignore
├── src
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── kamacoder
│ │ │ └── AppTest.java
│ └── main
│ │ └── java
│ │ └── com
│ │ └── kamacoder
│ │ ├── StressTest.java
│ │ └── SkipList.java
└── pom.xml
└── README.md
/skiplist/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/skiplist/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/skiplist/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/skiplist/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | !**/src/main/**/target/
4 | !**/src/test/**/target/
5 |
6 | ### IntelliJ IDEA ###
7 | .idea/modules.xml
8 | .idea/jarRepositories.xml
9 | .idea/compiler.xml
10 | .idea/libraries/
11 | *.iws
12 | *.iml
13 | *.ipr
14 |
15 | ### Eclipse ###
16 | .apt_generated
17 | .classpath
18 | .factorypath
19 | .project
20 | .settings
21 | .springBeans
22 | .sts4-cache
23 |
24 | ### NetBeans ###
25 | /nbproject/private/
26 | /nbbuild/
27 | /dist/
28 | /nbdist/
29 | /.nb-gradle/
30 | build/
31 | !**/src/main/**/build/
32 | !**/src/test/**/build/
33 |
34 | ### VS Code ###
35 | .vscode/
36 |
37 | ### Mac OS ###
38 | .DS_Store
--------------------------------------------------------------------------------
/skiplist/src/test/java/com/kamacoder/AppTest.java:
--------------------------------------------------------------------------------
1 | package com.kamacoder;
2 |
3 | import junit.framework.Test;
4 | import junit.framework.TestCase;
5 | import junit.framework.TestSuite;
6 |
7 | /**
8 | * Unit test for simple App.
9 | */
10 | public class AppTest
11 | extends TestCase
12 | {
13 | /**
14 | * Create the test case
15 | *
16 | * @param testName name of the test case
17 | */
18 | public AppTest( String testName )
19 | {
20 | super( testName );
21 | }
22 |
23 | /**
24 | * @return the suite of tests being tested
25 | */
26 | public static Test suite()
27 | {
28 | return new TestSuite( AppTest.class );
29 | }
30 |
31 | /**
32 | * Rigourous Test :-)
33 | */
34 | public void testApp()
35 | {
36 | assertTrue( true );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/skiplist/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | com.kamacoder
6 | skiplist
7 | 1.0-SNAPSHOT
8 | jar
9 |
10 | skiplist
11 | http://maven.apache.org
12 |
13 |
14 | UTF-8
15 |
16 |
17 |
18 |
19 | junit
20 | junit
21 | 3.8.1
22 | test
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skiplist-Java
2 |
3 | ## KV 存储引擎
4 |
5 | 众所周知,levedb,rockdb 其核心存储引擎的数据结构就是跳表。
6 |
7 | 在 Redis 中,跳表被用于实现有序集合(sorted sets)数据类型。
8 |
9 | 本项目就是基于跳表实现的轻量级键值型存储引擎,使用 Java 实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据等功能。
10 |
11 | ## 项目中文件
12 |
13 | * SkipList.java:跳表的核心实现
14 | * StressTest.java:对跳表进行压力测试
15 |
16 | ## 存储引擎数据表现
17 |
18 | * 跳表最大树高为 32
19 | * 在单线程环境下测试(多线程反而会消耗更多时间以及资源,因为在插入操作时进行加锁操作
20 | * 机器配置:MacOS(14.3.1) M1 芯片 + 16 GB 内存
21 |
22 | ### 插入操作
23 |
24 | | 插入数据规模(万条) | 耗时(毫秒) | QPS |
25 | | ----------------- | --------- | ------- |
26 | | 10 | 129 | 775,194 |
27 | | 50 | 935 | 534,759 |
28 | | 100 | 2198 | 454,959 |
29 |
30 | ### 读取操作
31 |
32 | | 读取数据规模(万条) | 耗时(毫秒) | QPS |
33 | | ----------------- | --------- | ------- |
34 | | 10 | 101 | 990,099 |
35 | | 50 | 813 | 615,006 |
36 | | 100 | 2130 | 469,484 |
37 |
--------------------------------------------------------------------------------
/skiplist/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
53 |
54 |
55 |
56 |
57 | 1708933172001
58 |
59 |
60 | 1708933172001
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/skiplist/src/main/java/com/kamacoder/StressTest.java:
--------------------------------------------------------------------------------
1 | package com.kamacoder;
2 |
3 | import java.util.Random;
4 |
5 | public class StressTest {
6 | public static final int INSERT_TIMES = 100000;
7 | public static final int SEARCH_TIMES = 100000;
8 | public static String generateRandomString() {
9 | // 定义可能出现在随机字符串中的字符
10 | String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
11 | // 指定字符串长度,可以根据需要修改
12 | int length = 10;
13 | // 使用StringBuilder来构建最终的字符串
14 | StringBuilder result = new StringBuilder(length);
15 | // 创建Random实例用于生成随机数
16 | Random random = new Random();
17 |
18 | for (int i = 0; i < length; i++) {
19 | // 生成一个随机索引值,用于从字符集中选择字符
20 | int index = random.nextInt(characters.length());
21 | // 将选择的字符添加到结果中
22 | result.append(characters.charAt(index));
23 | }
24 | return result.toString();
25 | }
26 | public static void main(String[] args) throws InterruptedException {
27 | int numberOfThreads = 10;
28 | // 记录所有任务开始前的时间
29 | long start = System.currentTimeMillis();
30 |
31 | Thread[] threads = new Thread[numberOfThreads];
32 | SkipList skipList = new SkipList<>();
33 |
34 | for (int i = 0; i < numberOfThreads; i++) {
35 | // 创建任务线程
36 | threads[i] = new Thread(new InsertTask(skipList));
37 | threads[i].start();
38 | }
39 |
40 | // 等待所有线程执行完毕
41 | for (int i = 0; i < numberOfThreads; i++) {
42 | threads[i].join();
43 | }
44 |
45 | // 所有线程都执行完毕,记录结束时间
46 | long end = System.currentTimeMillis();
47 | // 计算并打印总执行时间
48 | System.out.println("在 " + numberOfThreads + " 线程环境下,插入 " + (numberOfThreads * INSERT_TIMES) + " 次数据耗时为:" + (end - start) + "ms");
49 |
50 | // 压测搜索时间
51 | long start2 = System.currentTimeMillis();
52 |
53 | Thread[] threads2 = new Thread[numberOfThreads];
54 | for (int i = 0; i < numberOfThreads; i++) {
55 | threads2[i] = new Thread(new SearchTask(skipList));
56 | threads2[i].start();
57 | }
58 |
59 | for (int i = 0; i < numberOfThreads; i++) {
60 | threads2[i].join();
61 | }
62 |
63 | long end2 = System.currentTimeMillis();
64 | System.out.println("在 " + numberOfThreads + " 线程环境下,搜索 " + (numberOfThreads * SEARCH_TIMES) + " 次数据耗时为: " + (end2 - start2) + "ms");
65 |
66 | }
67 |
68 | private static class InsertTask, V> implements Runnable {
69 | SkipList skipList;
70 | InsertTask(SkipList skipList) {
71 | this.skipList = skipList;
72 | }
73 | @Override
74 | public void run() {
75 | for (int i = 0; i < INSERT_TIMES; i++) {
76 | boolean b = this.skipList.insertNode((K)generateRandomString(), (V)generateRandomString());
77 | }
78 | }
79 | }
80 |
81 | private static class SearchTask, V> implements Runnable {
82 | SkipList skipList;
83 | SearchTask(SkipList skipList) {
84 | this.skipList = skipList;
85 | }
86 | @Override
87 | public void run() {
88 | for (int i = 0; i < SEARCH_TIMES; i++) {
89 | this.skipList.searchNode((K)generateRandomString());
90 | }
91 | }
92 | }
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/skiplist/src/main/java/com/kamacoder/SkipList.java:
--------------------------------------------------------------------------------
1 | package com.kamacoder;
2 |
3 | import java.io.*;
4 | import java.util.ArrayList;
5 | import java.util.Collections;
6 | import java.util.Random;
7 | import java.util.Scanner;
8 |
9 | public class SkipList, V> {
10 | /**
11 | * Node 类,用于实际存储数据
12 | * @param
13 | * @param
14 | */
15 | private static class Node, V> {
16 | K key; // 存储的 key
17 | V value; // 存储的 value
18 | int level; // 节点所在的层级
19 | ArrayList> forwards;
20 |
21 | Node(K key, V value, int level) {
22 | this.key = key;
23 | this.value = value;
24 | this.level = level;
25 | this.forwards = new ArrayList<>(Collections.nCopies(level + 1, null));
26 | }
27 |
28 | public K getKey() {
29 | return key;
30 | }
31 |
32 | public V getValue() {
33 | return value;
34 | }
35 |
36 | public void setValue(V value) {
37 | this.value = value;
38 | }
39 | }
40 | /**
41 | * 跳表的最大高度
42 | */
43 | private static final int MAX_LEVEL = 32; // 跳表的最大高度
44 | /**
45 | * 跳表的头节点
46 | */
47 | private Node header; // 头节点
48 | /**
49 | * 跳表中的节点数量
50 | */
51 | private int nodeCount;
52 | /**
53 | * 跳表当前的层级
54 | */
55 | private int skipListLevel;
56 | /**
57 | * 跳表数据持久化路径
58 | */
59 | private static final String STORE_FILE = "./store";
60 |
61 | /**
62 | * 跳表构造方法
63 | */
64 | SkipList() {
65 | this.header = new Node<>(null, null, MAX_LEVEL);
66 | this.nodeCount = 0;
67 | this.skipListLevel = 0;
68 | }
69 |
70 | /**
71 | * 创建 Node 方法
72 | *
73 | * @param key 存入的键
74 | * @param value 存入的值
75 | * @param level 该节点所在的层级
76 | * @return 返回创建后的该节点
77 | */
78 | private Node createNode(K key, V value, int level) {
79 | return new Node<>(key, value, level);
80 | }
81 |
82 | /**
83 | * 生成 Node 所在层级方法
84 | * @return 返回节点层级
85 | */
86 | private static int generateRandomLevel() { // 生成随机层级方法
87 | int level = 1;
88 | Random random = new Random();
89 | while (random.nextInt(2) == 1) {
90 | level++;
91 | }
92 | return Math.min(level, MAX_LEVEL);
93 | }
94 |
95 | /**
96 | * @return 返回跳表中节点的数量
97 | */
98 | public int size() {
99 | return this.nodeCount;
100 | }
101 |
102 | /**
103 | * 向跳表中插入一个键值对,如果跳表中已经存在相同 key 的节点,则更新这个节点的 value
104 | * @param key 插入的 Node 的键
105 | * @param value 插入的 Node 的值
106 | * @return 返回插入结果,插入成功返回 true,插入失败返回 false
107 | */
108 | public synchronized boolean insertNode(K key, V value) {
109 | Node current = this.header;
110 | ArrayList> update = new ArrayList<>(Collections.nCopies(MAX_LEVEL + 1, null));
111 |
112 | for (int i = this.skipListLevel; i >= 0; i--) {
113 | while (current.forwards.get(i) != null && current.forwards.get(i).getKey().compareTo(key) < 0) {
114 | current = current.forwards.get(i);
115 | }
116 | update.set(i, current);
117 | }
118 |
119 | current = current.forwards.get(0);
120 |
121 | if (current != null && current.getKey().compareTo(key) == 0) { // 如果 key 已经存在
122 | // 更新 key 对应的 value
123 | current.setValue(value);
124 | return true;
125 | }
126 |
127 | // 生成节点随机层数
128 | int randomLevel = generateRandomLevel();
129 |
130 | if (current == null || current.getKey().compareTo(key) != 0) {
131 |
132 | if (randomLevel > skipListLevel) {
133 | for (int i = skipListLevel + 1; i < randomLevel + 1; i++) {
134 | update.set(i, header);
135 | }
136 | skipListLevel = randomLevel; // 更新跳表的当前高度
137 | }
138 |
139 | Node insertNode = createNode(key, value, randomLevel);
140 |
141 | // 修改跳表中的指针指向
142 | for (int i = 0; i <= randomLevel; i++) {
143 | insertNode.forwards.set(i, update.get(i).forwards.get(i));
144 | update.get(i).forwards.set(i, insertNode);
145 | }
146 | nodeCount++;
147 | return true;
148 | }
149 | return false;
150 | }
151 |
152 | /**
153 | * 搜索跳表中是否存在键为 key 的键值对
154 | * @param key 键
155 | * @return 跳表中存在键为 key 的键值对返回 true,不存在返回 false
156 | */
157 | public boolean searchNode(K key) {
158 | Node current = this.header;
159 |
160 | for (int i = this.skipListLevel; i >= 0; i--) {
161 | while (current.forwards.get(i) != null && current.forwards.get(i).getKey().compareTo(key) < 0) {
162 | current = current.forwards.get(i);
163 | }
164 | }
165 |
166 | current = current.forwards.get(0);
167 | return current != null && current.getKey().compareTo(key) == 0;
168 | }
169 |
170 | /**
171 | * 获取键为 key 的 Node 的值
172 | * @param key 键
173 | * @return 返回键为 key 的节点,如果不存在则返回 null
174 | */
175 | public V getNode(K key) {
176 | Node current = this.header;
177 |
178 | for (int i = this.skipListLevel; i >= 0; i--) {
179 | while (current.forwards.get(i) != null && current.forwards.get(i).getKey().compareTo(key) < 0) {
180 | current = current.forwards.get(i);
181 | }
182 | }
183 |
184 | current = current.forwards.get(0);
185 |
186 | if (current != null && current.getKey().compareTo(key) == 0) {
187 | return current.getValue();
188 | }
189 | // 这里有一个限制,存入的 key 和 value 必须是 Java 对象
190 | return null;
191 | }
192 |
193 | /**
194 | * 根据 key 删除 SkipList 中的 Node
195 | *
196 | * @param key 需要删除的 Node 的 key
197 | * @return 删除成功返回 true,失败返回 false
198 | */
199 | public synchronized boolean deleteNode(K key) {
200 | Node current = this.header;
201 | ArrayList> update = new ArrayList<>(Collections.nCopies(MAX_LEVEL + 1, null));
202 |
203 | for (int i = this.skipListLevel; i >= 0; i--) {
204 | while (current.forwards.get(i) != null && current.forwards.get(i).getKey().compareTo(key) < 0) {
205 | current = current.forwards.get(i);
206 | }
207 | update.set(i, current);
208 | }
209 |
210 | current = current.forwards.get(0);
211 |
212 | // 搜索到 key
213 | if (current != null && current.getKey().compareTo(key) == 0) {
214 | for (int i = 0; i < this.skipListLevel; i++) {
215 |
216 | if (update.get(i).forwards.get(i) != current) break;
217 |
218 | update.get(i).forwards.set(i, current.forwards.get(i));
219 | }
220 | }
221 |
222 | while (this.skipListLevel > 0 && this.header.forwards.get(this.skipListLevel) == null) {
223 | this.skipListLevel--;
224 | }
225 |
226 | this.nodeCount--;
227 | return true;
228 | }
229 |
230 | /**
231 | * 持久化跳表内的数据
232 | */
233 | public void dumpFile() {
234 | try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(STORE_FILE))) {
235 | Node node = this.header.forwards.get(0);
236 | while (node != null) {
237 | String data = node.getKey() + ":" + node.getValue() + ";";
238 | bufferedWriter.write(data);
239 | bufferedWriter.newLine();
240 | node = node.forwards.get(0);
241 | }
242 | } catch (IOException e) {
243 | throw new RuntimeException("Failed to dump file", e);
244 | }
245 | }
246 |
247 | /**
248 | * 从文本文件中读取数据
249 | */
250 | public void loadFile() {
251 | try (BufferedReader bufferedReader = new BufferedReader(new FileReader(STORE_FILE))) {
252 | String data;
253 | while ((data = bufferedReader.readLine()) != null) {
254 | System.out.println(data);
255 | Node node = getKeyValueFromString(data);
256 | if (node != null) {
257 | insertNode(node.getKey(), node.getValue());
258 | }
259 | }
260 | } catch (Exception e) {
261 | throw new RuntimeException(e);
262 | }
263 | }
264 |
265 | /**
266 | * 判断读取的字符串是否合法
267 | *
268 | * @param data 字符串
269 | * @return 合法返回 true,非法返回 false
270 | */
271 | private boolean isValidString(String data) {
272 | if (data == null || data.isEmpty()) {
273 | return false;
274 | }
275 | if (!data.contains(":")) {
276 | return false;
277 | }
278 | return true;
279 | }
280 |
281 | /**
282 | * 根据文件中的持久化字符串,获取 key 和 value,并将 key 和 value 封装到 Node 对象中
283 | * @param data 字符串
284 | * @return 返回该字符串对应的key和value 组成的 Node 实例,如果字符串非法,则返回 null
285 | */
286 | private Node getKeyValueFromString(String data) {
287 | if (!isValidString(data)) return null;
288 | String substring = data.substring(0, data.indexOf(":"));
289 | K key = (K) substring;
290 | // 去掉分号,不要结尾冒号
291 | String substring1 = data.substring(data.indexOf(":") + 1, data.length() - 1);
292 | V value = (V) substring1;
293 | return new Node(key, value, 1);
294 | }
295 |
296 | /**
297 | * 打印跳表的结构
298 | */
299 | public void displaySkipList() {
300 | // 从最上层开始向下遍历所有层
301 | for (int i = this.skipListLevel; i >= 0; i--) {
302 | Node node = this.header.forwards.get(i);
303 | System.out.print("Level " + i + ": ");
304 | // 遍历当前层的所有节点
305 | while (node != null) {
306 | // 打印当前节点的键和值,键值对之间用":"分隔
307 | System.out.print(node.getKey() + ":" + node.getValue() + ";");
308 | // 移动到当前层的下一个节点
309 | node = node.forwards.get(i);
310 | }
311 | // 当前层遍历结束,换行
312 | System.out.println();
313 | }
314 | }
315 |
316 | public static void main(String[] args) {
317 | SkipList skipList = new SkipList<>();
318 | Scanner scanner = new Scanner(System.in);
319 |
320 | while (true) {
321 | String command = scanner.nextLine();
322 | String[] commandList = command.split(" ");
323 | if (commandList[0].equals("insert")) {
324 | boolean b = skipList.insertNode(commandList[1], commandList[2]);
325 | if (b) {
326 | System.out.println("Key: " + commandList[1] + " Value: " + commandList[2] + " insert success!");
327 | } else {
328 | System.out.println("Key: " + commandList[1] + " Value: " + commandList[2] + " insert failed");
329 | }
330 | } else if (commandList[0].equals("delete")) {
331 | boolean b = skipList.deleteNode(commandList[1]);
332 | if (b) {
333 | System.out.println("Key: " + commandList[1] + " deleted!");
334 | } else {
335 | System.out.println("skiplist not exists the key: " + commandList[1]);
336 | }
337 | } else if (commandList[0].equals("search")) {
338 | boolean b = skipList.searchNode(commandList[1]);
339 | if (b) {
340 | System.out.println("Key: " + commandList[1] + " searched!");
341 | } else {
342 | System.out.println("Key: " + commandList[1] + " not exists!");
343 | }
344 | } else if (commandList[0].equals("get")) {
345 | if (!skipList.searchNode(commandList[1])) {
346 | System.out.println("Key: " + commandList[1] + " not exists!");
347 | }
348 | String node = skipList.getNode(commandList[1]);
349 | if (node != null) {
350 | System.out.println("Key: " + commandList[1] + "'s value is " + node);
351 | }
352 | } else if (commandList[0].equals("dump")) {
353 | skipList.dumpFile();
354 | System.out.println("Already saved skiplist.");
355 | } else if (commandList[0].equals("load")) {
356 | skipList.loadFile();
357 | } else {
358 | System.out.println("********skiplist*********");
359 | skipList.displaySkipList();
360 | System.out.println("*************************");
361 | }
362 | }
363 | }
364 | }
--------------------------------------------------------------------------------