feat(kv): 初始化 KV 服务模块

- 添加了笔记内容的增删查 DTO 类
- 配置了 Cassandra 数据库连接
- 实现了基于 Cassandra 的笔记内容存储与查询功能

feat(kv): 初始化 distributeID 服务模块

- 实现了分布式 ID 生成器服务(Snowflake与 Segment)
- 添加了 ID 生成器监控接口
- 配置了 MyBatis 与数据库交互
- 添加了 Segment 与 Snowflake 服务实现
- 添加了 Leaf 相关模型类与分配器接口
- 添加了 Leaf 分配器实现类
- 添加了 Leaf 控制器与监控视图
- 添加了 Leaf 异常处理类
- 添加了 Leaf 日志配置文件
- 添加了 Leaf 启动类
- 添加了 Leaf 常量定义
- 添加了 Leaf ID 生成接口
- 添加了 Leaf 初始化异常类
- 添加了 Leaf 配置文件
- 添加了 Leaf 模型类
- 添加了 Leaf 服务类
- 添加了 Leaf 工具类
- 添加了 Leaf 相关注解
- 添加了 Leaf 相关配置类
- 添加了 Leaf 相关枚举类
- 添加了 Leaf 相关工具类

后续考虑复刻Leaf代码至Java21平台
This commit is contained in:
Hanserwei
2025-10-06 22:28:27 +08:00
parent 534a49a358
commit 2910fdb54f
63 changed files with 2706 additions and 1 deletions

View File

@@ -0,0 +1,57 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-kv</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-kv-biz</artifactId>
<name>${project.artifactId}</name>
<description>Key-Value 键值存储业务层</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Cassandra 存储 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-kv-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.kv.biz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HannoteKVBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteKVBizApplication.class, args);
}
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.hannote.kv.biz.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
@Configuration
public class CassandraConfig extends AbstractCassandraConfiguration {
@Value("${spring.cassandra.keyspace-name}")
private String keySpace;
@Value("${spring.cassandra.contact-points}")
private String contactPoints;
@Value("${spring.cassandra.port}")
private int port;
@Override
protected String getKeyspaceName() {
return keySpace;
}
@Override
public String getContactPoints() {
return contactPoints;
}
@Override
public int getPort() {
return port;
}
}

View File

@@ -0,0 +1,40 @@
package com.hanserwei.hannote.kv.biz.controller;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.biz.service.NoteContentService;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.FindNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.resp.FindNoteContentRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/kv")
@Slf4j
public class NoteContentController {
@Resource
private NoteContentService noteContentService;
@PostMapping(value = "/note/content/add")
public Response<?> addNoteContent(@Validated @RequestBody AddNoteContentReqDTO addNoteContentReqDTO) {
return noteContentService.addNoteContent(addNoteContentReqDTO);
}
@PostMapping(value = "/note/content/find")
public Response<FindNoteContentRspDTO> findNoteContent(@Validated @RequestBody FindNoteContentReqDTO findNoteContentReqDTO) {
return noteContentService.findNoteContent(findNoteContentReqDTO);
}
@PostMapping(value = "/note/content/delete")
public Response<?> deleteNoteContent(@Validated @RequestBody DeleteNoteContentReqDTO deleteNoteContentReqDTO) {
return noteContentService.deleteNoteContent(deleteNoteContentReqDTO);
}
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.hannote.kv.biz.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
import java.util.UUID;
@Table("note_content")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NoteContentDO {
@PrimaryKey("id")
private UUID id;
private String content;
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.kv.biz.domain.repository;
import com.hanserwei.hannote.kv.biz.domain.dataobject.NoteContentDO;
import org.springframework.data.cassandra.repository.CassandraRepository;
import java.util.UUID;
public interface NoteContentRepository extends CassandraRepository<NoteContentDO, UUID> {
}

View File

@@ -0,0 +1,24 @@
package com.hanserwei.hannote.kv.biz.enums;
import com.hanserwei.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 -----------
SYSTEM_ERROR("KV-10000", "出错啦,后台小哥正在努力修复中..."),
PARAM_NOT_VALID("KV-10001", "参数错误"),
// ----------- 业务异常状态码 -----------
NOTE_CONTENT_NOT_FOUND("KV-20000", "该笔记内容不存在"),
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMsg;
}

View File

@@ -0,0 +1,103 @@
package com.hanserwei.hannote.kv.biz.exception;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.biz.enums.ResponseCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Optional;
@SuppressWarnings("LoggingSimilarMessage")
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获自定义业务异常
*
* @return Response.fail(e)
*/
@ExceptionHandler({ApiException.class})
@ResponseBody
public Response<Object> handleApiException(HttpServletRequest request, ApiException e) {
log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMsg());
return Response.fail(e);
}
/**
* 捕获参数校验异常
*
* @return Response.fail(errorCode, errorMessage)
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 获取 BindingResult
BindingResult bindingResult = e.getBindingResult();
StringBuilder sb = new StringBuilder();
// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';
Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> {
errors.forEach(error ->
sb.append(error.getField())
.append(" ")
.append(error.getDefaultMessage())
.append(", 当前值: '")
.append(error.getRejectedValue())
.append("'; ")
);
});
// 错误信息
String errorMessage = sb.toString();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 捕获 guava 参数校验异常
*
* @return Response.fail(ResponseCodeEnum.PARAM_NOT_VALID)
*/
@ExceptionHandler({IllegalArgumentException.class})
@ResponseBody
public Response<Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 错误信息
String errorMessage = e.getMessage();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 其他类型异常
*
* @param request 请求
* @param e 异常
* @return Response.fail(ResponseCodeEnum.SYSTEM_ERROR)
*/
@ExceptionHandler({Exception.class})
@ResponseBody
public Response<Object> handleOtherException(HttpServletRequest request, Exception e) {
log.error("{} request error, ", request.getRequestURI(), e);
return Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
}
}

View File

@@ -0,0 +1,34 @@
package com.hanserwei.hannote.kv.biz.service;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.FindNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.resp.FindNoteContentRspDTO;
public interface NoteContentService {
/**
* 添加笔记内容
*
* @param addNoteContentReqDTO 添加笔记内容请求参数
* @return 响应
*/
Response<?> addNoteContent(AddNoteContentReqDTO addNoteContentReqDTO);
/**
* 查询笔记内容
*
* @param findNoteContentReqDTO 查询笔记内容请求参数
* @return 响应
*/
Response<FindNoteContentRspDTO> findNoteContent(FindNoteContentReqDTO findNoteContentReqDTO);
/**
* 删除笔记内容
*
* @param deleteNoteContentReqDTO 删除笔记内容请求参数
* @return 响应
*/
Response<?> deleteNoteContent(DeleteNoteContentReqDTO deleteNoteContentReqDTO);
}

View File

@@ -0,0 +1,67 @@
package com.hanserwei.hannote.kv.biz.service.impl;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.biz.domain.dataobject.NoteContentDO;
import com.hanserwei.hannote.kv.biz.domain.repository.NoteContentRepository;
import com.hanserwei.hannote.kv.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.kv.biz.service.NoteContentService;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.FindNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.resp.FindNoteContentRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
@Service
@Slf4j
public class NoteContentServiceImpl implements NoteContentService {
@Resource
private NoteContentRepository noteContentRepository;
@Override
public Response<?> addNoteContent(AddNoteContentReqDTO addNoteContentReqDTO) {
// 笔记ID
Long noteId = addNoteContentReqDTO.getNoteId();
// 笔记内容
String content = addNoteContentReqDTO.getContent();
NoteContentDO noteContent = NoteContentDO.builder()
.id(UUID.randomUUID())
.content(content)
.build();
// 插入数据
noteContentRepository.save(noteContent);
return Response.success();
}
@Override
public Response<FindNoteContentRspDTO> findNoteContent(FindNoteContentReqDTO findNoteContentReqDTO) {
// 笔记ID
String noteId = findNoteContentReqDTO.getNoteId();
Optional<NoteContentDO> optional = noteContentRepository.findById(UUID.fromString(noteId));
if (optional.isEmpty()){
throw new ApiException(ResponseCodeEnum.NOTE_CONTENT_NOT_FOUND);
}
NoteContentDO noteContentDO = optional.get();
// 构建回参
FindNoteContentRspDTO findNoteContentRspDTO = FindNoteContentRspDTO.builder()
.noteId(noteContentDO.getId())
.content(noteContentDO.getContent())
.build();
return Response.success(findNoteContentRspDTO);
}
@Override
public Response<?> deleteNoteContent(DeleteNoteContentReqDTO deleteNoteContentReqDTO) {
String noteId = deleteNoteContentReqDTO.getNoteId();
noteContentRepository.deleteById(UUID.fromString(noteId));
return Response.success();
}
}

View File

@@ -0,0 +1,6 @@
server:
port: 8084 # 项目启动的端口
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境

View File

@@ -0,0 +1,12 @@
spring:
application:
name: han-note-kv # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 NaCos 配置中心的服务器地址

View File

@@ -0,0 +1,58 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="kv"/>
<!-- 自定义日志输出路径,以及日志名称前缀 -->
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<!-- 每行日志输出的格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 本地 dev 开发环境 -->
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/> <!-- 输出控制台日志 -->
<appender-ref ref="ASYNC_FILE"/> <!-- 打印日志到文件中。PS: 本地环境下,如果不想打印日志到文件,可注释掉此行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,65 @@
package com.hanserwei.hannote.kv.biz;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.kv.biz.domain.dataobject.NoteContentDO;
import com.hanserwei.hannote.kv.biz.domain.repository.NoteContentRepository;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
import java.util.UUID;
@SpringBootTest
@Slf4j
class CassandraTests {
@Resource
private NoteContentRepository noteContentRepository;
/**
* 测试插入数据
*/
@Test
void testInsert() {
NoteContentDO nodeContent = NoteContentDO.builder()
.id(UUID.randomUUID())
.content("代码测试笔记内容插入")
.build();
noteContentRepository.save(nodeContent);
}
/**
* 测试修改数据
*/
@Test
void testUpdate() {
NoteContentDO nodeContent = NoteContentDO.builder()
.id(UUID.fromString("8a0e491d-49f0-41cf-95b1-c9f541567156"))
.content("代码测试笔记内容更新")
.build();
noteContentRepository.save(nodeContent);
}
/**
* 测试查询数据
*/
@Test
void testSelect() {
Optional<NoteContentDO> optional = noteContentRepository.findById(UUID.fromString("8a0e491d-49f0-41cf-95b1-c9f541567156"));
optional.ifPresent(noteContentDO -> log.info("查询结果:{}", JsonUtils.toJsonString(noteContentDO)));
}
/**
* 测试删除数据
*/
@Test
void testDelete() {
noteContentRepository.deleteById(UUID.fromString("8a0e491d-49f0-41cf-95b1-c9f541567156"));
}
}