├── 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 | 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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 29 | 30 | 31 | 33 | 36 | 37 | 38 | 41 | 53 | 54 | 55 | 56 | 57 | 1708933172001 58 | 63 | 64 | 65 | 66 | 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 | } --------------------------------------------------------------------------------