微服务项目->在线oj系统(Java版 - 3)
相信自己,终会成功
目录
管理端代码
管理端管理用户接口
有关token的代码解析
前缀裁剪原理
管理员登录代码
题目接口详解
题目相关接口详解
竞赛接口详解
MyBatis分页插件
PageHelper实现
原理分析
Java Stream 流
1. Stream 的核心特点
1. 中间操作(Intermediate Operations)
2. 终端操作(Terminal Operations)
Elasticsearch
优点
缺点
管理端代码
管理端管理用户接口
管理员登录之后,生成一个token令牌,之后携带令牌调用各种接口
添加用户时,需要先验证用户是否存在,存在的话就会报错
public String createToken(Long userId,String secret,Integer identity,String nickName,String headInmage){//生成JWT token的方法Map<String,Object> claims=new HashMap<>();String userKey= UUID.fastUUID().toString();claims.put(JwtConstants.LOGIN_USER_ID,userId);claims.put(JwtConstants.LOGIN_USER_KEY,userKey);// secret定期更换,不能写死,写在nacos上String token= JwtUtils.createToken(claims,secret);//往第三方存储敏感信息 redis//身份认证具体要存储哪些信息表明用户身份字段 identity 1 表示普通用户 2表示管理员用户//但是最好是可扩展的,所以设置为对象//使用什么样的数据结构 String key-value//key必须保证唯一,便于维护=>统一前缀+logintoken:userId userId通过雪花算法生成的唯一的字符串//主键若为自增,redis存储所用用户登录信息(包括管理员和C端用户)//hutool提供的工具类(uuid可以理解为全局唯一标识)String tokenkey = getTokenKey(userKey);
// String key ="logintoken:" + sysUser.getUserId();//记录过期时间,过期时间定义多长 Redis如果能查出来则没过期,不存在则过期LoginUser loginUSer=new LoginUser();loginUSer.setIdentity(identity);loginUSer.setNickName(nickName);loginUSer.setHeadImage(headInmage);redisService.setCacheObject(tokenkey,loginUSer, CacheConstants.EXP, TimeUnit.MINUTES);return token;}
有关token的代码解析
filter方法,获取http请求,之后调用了ServerHttpRequest.getURI().getPath()
方法,其返回值是请求的路径部分。
举个例子,如果完整的请求 URL 是http://example.com/api/users/123
,那么getPath()
返回的就是/api/users/123
。
判断这个url是否在白名单中,如果匹配则会直接放行该请求
不会则会继续判断,走到getToken函数 , 从请求头中获取JWR令牌,判断令牌是否存在,并且移除前缀,
replaceFirst
方法会用空字符串替换掉第一个匹配到的前缀。
经过这一步处理后,Bearer eyJhbGciOiJIUzI1NiJ9...
就会变成eyJhbGciOiJIUzI1NiJ9...
前缀就会被移除了
前缀裁剪原理
- 正则表达式匹配:
replaceFirst
的第一个参数实际上是一个正则表达式,不过像"Bearer "
这样的普通字符串也能被正确匹配。 - 只替换首个匹配项:该方法只会替换第一次出现的前缀,这样就能避免误处理令牌中可能重复出现的相同字符序列。
- 空格处理:前缀中的空格很关键,它能确保
Bearer
和真正的令牌内容分隔开
假设:
HttpConstants.PREFIX
的值为"Bearer "
。- 客户端发送的请求头是
Authorization: Bearer abc123
。执行过程如下:
- 从请求头获取到的原始令牌是
"Bearer abc123"
。- 因为这个令牌以
"Bearer "
开头,所以会进入if
语句块。- 执行
replaceFirst
后,得到的结果是"abc123"
。
之后判断token 是否为空,如果为空抛出异常
为什么不用 throw new RuntimeException而选择返回特定响应??
1.响应式编程模型的要求:需通过异步数据流返回结果,而非同步抛出异常。
2.精确控制 HTTP 响应:直接设置状态码和错误信息,符合业务语义(如 401 Unauthorized)。
3.避免破坏过滤器链:保证请求处理流程正常终止,不影响其他过滤器逻辑。
获取token的中间部分( Payload ) ,获取令牌中信息 解析payload中信息 存储着用户的唯一表示信息
之后获取jwt中的key,判断key是否过期
如果走到下述代码则说明token正确,且未过期
LoginUser user = redisService.getCacheObject(getTokenKey(userKey), LoginUser.class);
再判断当前请求 请求的时C端功能(只有C端用户可以请求) 还是B端功能(只有管理员可以请求)
拿出存储信息,判断是管理员还是用户,确实身份后进行延长token(前提token少于规定的时间)
@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//获取http请求ServerHttpRequest request = exchange.getRequest();String url = request.getURI().getPath();//请求接口地址 登录接口是否需要身份认证? 否// 跳过不需要验证的路径 接口白名单,通过白名单的接口均不需要进行身份验证if (matches(url, ignoreWhite.getWhites())) {//判断当前接口如果在白名单中则不需要身份认证return chain.filter(exchange); //ignoreWhite.getWhites():拿到nacos上配置接口地址的白名单}//执行到这里,说明接口不在白名单上,接着进行身份认证//通过token进行身份认证,客户端会把token放在请求头中,需要把token获取出来//从http请求头中获取tokenString token = getToken(request);if (StrUtil.isEmpty(token)) {//为什么不用 throw new RuntimeExceptionreturn unauthorizedResponse(exchange, "令牌不能为空");}Claims claims;try {
// Payload是JWT中最重要的部分,因为它包含了令牌携带的实际信息,这些信息可以在不接触数据库的情况下验证用户身份和权限
// 在JWT(JSON Web Token)中,payload是指令牌的中间部分,包含了令牌携带的实际数据(也称为"声明"claims)。claims = JwtUtils.parseToken(token, secret); //获取令牌中信息 解析payload中信息 存储着用户的唯一表示信息if (claims == null) {//soringcloud gateway基于webfluxreturn unauthorizedResponse(exchange, "令牌已过期或验证不正确!");}} catch (Exception e) {return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");}
// String key ="logintoken:" + sysUser.getUserId();这个方法存储的key,可以这样取值
// String userId = JwtUtils.getUserId(claims);
// boolean isLogin = redisService.hasKey(getTokenKey(userId));//控制token有效时间,判断redis存储的敏感信息到期,是否存在String userKey = JwtUtils.getUserKey(claims); //获取jwt中的keyboolean isLogin = redisService.hasKey(getTokenKey(userKey));if (!isLogin) {return unauthorizedResponse(exchange, "登录状态已过期");}String userId = JwtUtils.getUserId(claims); //判断jwt中的信息是否完整if (StrUtil.isEmpty(userId)) {return unauthorizedResponse(exchange, "令牌验证失败");}//token正确,并且没有过期//判断redis存储的关于用户身份认证信息是否正确//判断当前请求 请求的时C端功能(只有C端用户可以请求) 还是B端功能(只有管理员可以请求)//拿出存储信息//url.contains,前缀如果是Steam,访问system接口// 前缀如果是friend,访问friend接口LoginUser user = redisService.getCacheObject(getTokenKey(userKey), LoginUser.class);if (url.contains(HttpConstants.SYSTEM_URL_PREFIX) &&!UserIdentity.ADMIN.getValue().equals(user.getIdentity())) {return unauthorizedResponse(exchange, "令牌验证失败");}if (url.contains(HttpConstants.FRIEND_URL_PREFIX) &&!UserIdentity.ORDINARY.getValue().equals(user.getIdentity())) {return unauthorizedResponse(exchange, "令牌验证失败");}
// 将当前用户的ID(userId)存储到线程本地变量(ThreadLocal)中
// 并以常量Constants.USER_ID作为键名。
// 如果写在这里,存在线程隔离 common和gateway是两个微服务,
// 所以换到TokenInterceptor中了//ThreadLocalUtil.set(Constants.USER_ID,userId);//确定身份认证之后,进行时间延长return chain.filter(exchange);}
获取token
/*** 从请求头中获取请求token*/private String getToken(ServerHttpRequest request) {String token = request.getHeaders().getFirst(HttpConstants.AUTHENTICATION);// 如果前端设置了令牌前缀,则裁剪掉前缀if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);}return token;}
查找指定url是否匹配指定匹配规则链表中的任意⼀个字符串
/*** 查找指定url是否匹配指定匹配规则链表中的任意⼀个字符串** @param url 指定url* @param patternList 需要检查的匹配规则链表* @return 是否匹配*/private boolean matches(String url, List<String> patternList) {
//接口匹配if (StrUtil.isEmpty(url) || CollectionUtils.isEmpty(patternList)) {return false;}
//接口地址如果和白名单其中一个地址匹配就返回true
// 如果遍历完白名单所有地址都没有匹配返回falsefor (String pattern : patternList) {if (isMatch(pattern, url)) {//匹配上了return true;}}return false;}
记录过期时间,过期时间定义多长 Redis如果能查出来则没过期,不存在则过期
/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Longtimeout, final TimeUnit timeUnit) {redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 判断 key是否存在** @param key 键* @return true 存在 false不存在*/public Boolean hasKey(String key) {return redisTemplate.hasKey(key);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key, Class<T> clazz) {ValueOperations<String, T> operation = redisTemplate.opsForValue();T t = operation.get(key);if (t instanceof String) {return t;}return JSON.parseObject(String.valueOf(t), clazz);}
管理员登录代码
public boolean logout(String token) {//检查token是否为空if (StrUtil.isEmpty(token)) {throw new IllegalArgumentException("token不为null");}//再处理前缀if (token.startsWith(HttpConstants.PREFIX)) {token = token.substring(HttpConstants.PREFIX.length());}return tokenService.deleteLoginUser(token,secret);}
退出登录代码,主要逻辑是接收一个 token,处理可能存在的前缀,然后调用 tokenService 的 deleteLoginUser 方法删除用户的登录状态
@Overridepublic boolean logout(String token) {//检查token是否为空if (StrUtil.isEmpty(token)) {throw new IllegalArgumentException("token不为null");}//再处理前缀if (token.startsWith(HttpConstants.PREFIX)) {token = token.substring(HttpConstants.PREFIX.length());}return tokenService.deleteLoginUser(token,secret);}
题目接口详解
获取题目列表
使用 String.split()
方法按分隔符(通常是 ;
)将字符串拆分为数组。
Constants.SPLIT_SEM
是自定义常量,代表分隔符(如 ";"
)。
代码中涉及一个知识点Java Stream 流(下方有介绍)
excludeIdSet = Arrays.stream(excludeIdStr.split(Constants.SPLIT_SEM)).filter(StrUtil::isNotBlank) // 过滤空字符串(避免 "1,,2" 的情况).map(Long::valueOf) // 转为 Long.collect(Collectors.toSet()); // 收集为 Set
对上述代码详解
Arrays.stream()
:将数组转换为流(Stream)
filter()
:过滤空字符串(处理 "1;;2"
这种情况)
StrUtil::isNotBlank
:引用工具类方法判断字符串非空(来自 Hutool 或类似库)。
map()
:将每个字符串元素转换为 Long
类型
Long::valueOf
:引用 Long
类的静态方法进行类型转换。
collect(Collectors.toSet())
:将流中的元素收集到 Set
中,自动去重
@Overridepublic List<QuestionVO> list(QuestionQueryDTO questionQueryDTO) {String excludeIdStr = questionQueryDTO.getExcludeIdStr();Set<Long> excludeIdSet = new HashSet<>(); // 默认空集合if (StrUtil.isNotEmpty(excludeIdStr)) {excludeIdSet = Arrays.stream(excludeIdStr.split(Constants.SPLIT_SEM)).filter(StrUtil::isNotBlank) // 过滤空字符串(避免 "1,,2" 的情况).map(Long::valueOf) // 转为 Long.collect(Collectors.toSet()); // 收集为 SetquestionQueryDTO.setExcludeIdSet(excludeIdSet);}// 自动分页:PageHelper 会自动解析你执⾏的 SQL 语句,并根据你提供的⻚码和每页显示的记录数,
// 自动添加分页相关的 SQL ⽚段,从而返回正确的分页结果。无需在 SQL 语句中手动编写复杂的分页逻辑PageHelper.startPage(questionQueryDTO.getPageNum(),questionQueryDTO.getPageSize());return questionMapper.selectQuestionList(questionQueryDTO);}
题目相关接口详解
管理员添加题目
@Overridepublic boolean add(QuestionAddDTO questionAddDTO) {
// List<Question> questionList=questionMapper.selectList(
// new LambdaQueryWrapper<Question>()
// .eq(Question::getTitle,QuestionAddDTO.getTitle()));List<Question> questionList = questionMapper.selectList(new LambdaQueryWrapper<Question>().eq(Question::getTitle, questionAddDTO.getTitle()) // 这里修正); if(CollectionUtil.isNotEmpty(questionList)){throw new ServiceException(ResultCode.FAILED_ALREADY_EXISTS);}Question question=new Question();// questionAddDTO:源对象(Source),通常是数据传输对象(DTO),包含前端传入的数据。
// question: 目标对象(Target),通常是一个实体类(Entity),用于后续的数据库操作或其他业务逻辑。
// 将前端传入的 DTO 对象转换为数据库实体对象。对象属性拷贝:避免手动逐个 set 属性,简化代码。// 将DTO属性复制到实体类BeanUtils.copyProperties(questionAddDTO, question);// 插入数据库int insert = questionMapper.insert(question);// 检查插入是否成功if(insert <= 0) {return false;}QuestionES questionES=new QuestionES();//往es(Elasticsearch)中添加BeanUtil.copyProperties(question,questionES);questionRepository.save(questionES);questionCacheManager.addCache(question.getQuestionId());return true;}
Question::getTitle
:通过方法引用指定实体类的字段(对应数据库的 title
列)
questionAddDTO.getTitle()
:实际查询的值(来自 DTO 的标题)
List<Question> questionList = questionMapper.selectList(new LambdaQueryWrapper<Question>().eq(Question::getTitle, questionAddDTO.getTitle()) // 这里修正);
将 questionAddDTO
对象的属性值复制到 question
对象中。
仅复制属性名相同且类型兼容的字段。
BeanUtils.copyProperties(questionAddDTO,question);
可能结果:
存在匹配记录:返回包含所有匹配记录的列表(可能多条,因为标题可能重复)
无匹配记录:返回空列表 []
(不会返回 null
)
异常情况:若 SQL 执行出错(如字段名错误、数据库连接问题),抛出异常。
下方有关于ES的介绍
// 创建ES文档对象
QuestionES questionES = new QuestionES();
// 将数据库实体属性复制到ES文档对象
BeanUtil.copyProperties(question, questionES);
// 保存到Elasticsearch
questionRepository.save(questionES);
删除题目数据
@Overridepublic int delete(Long questionId) {Question Question=questionMapper.selectById(questionId);if(Question==null){throw new ServiceException(ResultCode.FAILED_NOT_EXISTS);}//在es中删除数据questionRepository.deleteById(questionId);questionCacheManager.deleteCache(questionId);return questionMapper.deleteById(questionId);}
这个是实现题目的接口,至于edit和detail没有涉及特殊知识点就不介绍了
public interface IQuestionService {List<QuestionVO> list(QuestionQueryDTO questionQueryDTO);boolean add(QuestionAddDTO questionAddDTO);QuestionDetialVO detail(Long questionId);int edit(QuestionEditDTO questionEditDTO);int delete(Long questionId);
}
竞赛接口详解
添加题目接口
从前端拿到数据,checkExamSaveParams进行判断
在数据库中查询内容
检查考试标题是否重复(排除当前考试) , 验证开始时间和结束时间是否存在
验证开始时间是否早于当前时间 , 验证开始时间是否早于结束时间
如果没有出异常信息则正常,将前端拿到的数据复制到exam中,进而添加到数据库中
@Overridepublic String add(ExamAddDTO examAddDTO) {checkExamSaveParams(examAddDTO,null);Exam exam = new Exam();BeanUtils.copyProperties(examAddDTO, exam);examMapper.insert(exam);
//返回新创建考试的ID字符串形式return exam.getExamId().toString();}
private void checkExamSaveParams(ExamAddDTO examSaveDTO,Long examId) {List<Exam> examList = examMapper.selectList(new LambdaQueryWrapper<Exam>().eq(Exam::getTitle, examSaveDTO.getTitle()).ne(examId!=null,Exam::getExamId,examId));if (CollectionUtil.isNotEmpty(examList)) {throw new ServiceException(ResultCode.EXAM_RESULT_REPEAT);}if (examSaveDTO.getStartTime() == null || examSaveDTO.getEndTime() == null) {throw new ServiceException(ResultCode.EXAM_START_EXAM_TIME_NOT_EXIST);}if (examSaveDTO.getStartTime().isBefore((LocalDateTime.now()))) {throw new ServiceException(ResultCode.EXAM_START_TIME_BEFORE_CURRENT_TIME);//竞赛开始时间不能早于当前时间}if (examSaveDTO.getStartTime().isAfter(examSaveDTO.getEndTime())) {throw new ServiceException(ResultCode.EXAM_START_TIME_AFTER_END_TIME);//竞赛开始时间不能早于竞赛结束时间}}
在书写竞赛接口时需要注意
检查考试标题是否重复(排除当前考试) , 验证开始时间和结束时间是否存在
验证开始时间是否早于当前时间 , 验证开始时间是否早于结束时间
添加题目时,注意竞赛是否已经发布,如果已经发布则禁止编辑,在添加题目的时候,可以限制添加题数
发布和未发布就是需要把状态修改一下(数据库中内容也需要更新),在添加题目时,注意这个题目是否已被添加,如果以备添加,就不需要显示了
给竞赛添加题目,详情看注释(添加题目顺序)
@Overridepublic boolean questionAdd(ExamQuestionAddDTO examQuestionAddDTO) {//判断竞赛是否存在Exam exam = getExam(examQuestionAddDTO.getExamId());checkExam(exam);if(Constants.TRUE.equals(exam.getStatus())){throw new ServiceException(ResultCode.EXAM_IS_PUBLISH);}// 2. 从DTO中获取题目ID集合Set<Long> questionIdSet = examQuestionAddDTO.getQuestionIdSet();// 3. 如果题目ID集合为空,直接返回trueif (CollectionUtil.isEmpty(questionIdSet)) {return true;}// 4. 根据ID集合批量查询题目List<Question> questionList = questionMapper.selectBatchIds(questionIdSet);// 4. 根据ID集合批量查询题目if (CollectionUtil.isEmpty(questionList) || questionList.size() < questionIdSet.size()) {throw new ServiceException(ResultCode.EXAM_QUESTION_NOT_EXIST);}return saveExamQuestion(exam, questionIdSet);}
private boolean saveExamQuestion(Exam exam, Set<Long> questionIdSet) {// 初始化题目序号int num = 1;// 创建考试题目关联列表List<ExamQuestion> examQuestionList = new ArrayList<>();// 遍历题目ID集合for (Long questionId : questionIdSet) {ExamQuestion examQuestion = new ExamQuestion();// 设置考试IDexamQuestion.setExamId(exam.getExamId());// 设置题目IDexamQuestion.setQuestionId(questionId);// 设置题目顺序(自动递增)examQuestion.setQuestionOrder(num++);// 添加到列表examQuestionList.add(examQuestion);}//批量操作的方式return saveBatch(examQuestionList);}
MyBatis分页插件
分页是数据处理中的基础功能,良好的分页机制能显著提升系统性能和用户体验。以下是对分页管理机制的深度分析。
基本参数设计
参数名 | 说明 | 推荐默认值 |
---|---|---|
pageNum | 当前页码(从1开始) | 1 |
pageSize | 每页记录数 | 10-50(视业务而定) |
total | 总记录数 | - |
pages | 总页数 | 自动计算 |
orderBy | 排序字段(可选) | "create_time desc" |
逻辑分页(内存分页)
// Java示例:使用List.subList
List<Data> allData = getFromCacheOrDB();
List<Data> pageData = allData.subList(start, end);
-
优点:实现简单,适合小数据量
-
缺点:数据量大时内存消耗高
-
适用场景:本地缓存数据分页
物理分页(数据库分页)
-- MySQL
SELECT * FROM table LIMIT 10 OFFSET 20;-- Oracle
SELECT * FROM (SELECT t.*, ROWNUM rn FROM table t WHERE ROWNUM <= 30
) WHERE rn > 20;
-
优点:性能高,节省内存
-
缺点:SQL语法差异大
-
适用场景:大数据量查询
主流技术实现方案
PageHelper实现
// 1. 引入依赖
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>最新版</version>
</dependency>// 2. 使用示例
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
原理分析
-
通过ThreadLocal绑定分页参数
-
自动改写SQL添加方言特定的分页语句
-
执行count查询获取总数
优化手段 | 实现方式 | 适用场景 |
---|---|---|
延迟关联 | 先查ID再关联详情 | 多表关联查询 |
游标分页 | 使用WHERE id > lastId LIMIT size | 无限滚动列表 |
缓存总条数 | 缓存count结果,设置短过期时间 | 数据变化不频繁 |
避免count查询 | 仅"下一页"按钮时不做count | 移动端分页 |
Java Stream 流
Stream(流) 是 Java 8 引入的一个强大的数据处理 API,它允许你以声明式方式(类似 SQL 语句)对集合(Collection)、数组等数据进行高效、简洁的操作(如过滤、映射、排序、聚合等)。
Stream 的核心作用是简化集合操作、提升代码可读性,并通过惰性求值(中间操作(如 filter
、map
)不会立即执行,直到遇到终端操作(如 collect
、forEach
),避免不必要的计算)和并行处理优化性能。
1. Stream 的核心特点
-
非数据结构:不存储数据,只是数据源的视图
-
函数式编程:支持 lambda 表达式和方法引用
-
惰性执行:中间操作不会立即执行
-
可消费性:流只能被消费一次
2. Stream 的操作类型
Stream 的操作分为 中间操作(Intermediate) 和 终止操作(Terminal):
操作类型 | 说明 | 示例方法 |
中间操作 | 返回新的 Stream,可链式调用 | filter(), map(), sorted(), distinct(), limit() |
终止操作 | 触发计算并返回结果(非 Stream) | collect(), forEach(), count(), reduce(), anyMatch() |
1. 中间操作(Intermediate Operations)
操作 | 说明 | 示例 |
---|---|---|
filter | 过滤元素 | .filter(s -> s.length() > 3) |
map | 元素转换 | .map(String::toUpperCase) |
flatMap | 扁平化流 | .flatMap(list -> list.stream()) |
distinct | 去重 | .distinct() |
sorted | 排序 | .sorted(Comparator.reverseOrder()) |
peek | 查看元素但不修改 | .peek(System.out::println) |
limit | 限制元素数量 | .limit(10) |
skip | 跳过前N个元素 | .skip(5) |
2. 终端操作(Terminal Operations)
操作 | 说明 | 示例 |
---|---|---|
forEach | 遍历每个元素 | .forEach(System.out::println) |
collect | 收集为集合 | .collect(Collectors.toList()) |
toArray | 转为数组 | .toArray(String[]::new) |
reduce | 归约操作 | .reduce(0, Integer::sum) |
min/max | 查找最小/最大值 | .max(Comparator.naturalOrder()) |
count | 统计元素数量 | .count() |
anyMatch | 任意元素匹配 | .anyMatch(s -> s.startsWith("a")) |
allMatch | 所有元素匹配 | .allMatch(s -> s.length() > 2) |
noneMatch | 没有元素匹配 | .noneMatch(Objects::isNull) |
findFirst | 返回第一个元素 | .findFirst() |
findAny | 返回任意元素 | .findAny() |
Elasticsearch
Elastic Docs | Elastic官方网站:
Elastic Docs | Elastic
Elasticsearch(简称 ES)是一个开源的分布式 搜索和分析引擎,基于 Apache Lucene 构建,专为处理海量数据设计,支持近实时(NRT, Near Real-Time)搜索。
优点
-
高性能搜索,支持复杂查询(全文检索、模糊匹配、聚合分析)。
-
水平扩展能力强,适合大数据场景。
-
生态完善(ELK Stack、APM、SIEM 等)。
缺点
-
不支持事务(不适合金融级一致性要求场景)。
-
资源消耗较高(尤其是内存)。
-
学习曲线较陡(需理解分词、映射、集群管理等)
术语 | 说明 |
---|---|
Index(索引) | 类似数据库中的“表”,存储相关文档(如 user_index 、product_index )。 |
Document(文档) | 索引中的基本数据单元,使用 JSON 格式存储(如一条用户信息)。 |
Type(类型) | 7.x 版本后已弃用,现一个索引仅支持一种类型(_doc )。 |
Shard(分片) | 索引被拆分为多个分片,提高并发和存储能力(如一个索引拆分为 5 个分片)。 |
Replica(副本) | 分片的备份,提高容错能力(如每个分片有 1 个副本)。 |
Node(节点) | 一个 Elasticsearch 运行实例,多个节点组成集群(Cluster)。 |