Java 项目实战: 交友项目 - 发布圈子动态:数据库表和接口文档介绍

概述

发布动态这个功能要动两样东西:一个 movement 集合、一个 POST /movements 接口。这里把这两样一次讲透,字段为什么这么设计、媒体数据为什么嵌进文档而不是拆表、带文件上传的表单接口和之前见过的纯 JSON 接口差在哪。

纲要

  • 数据模型
  • movement 集合:Movement 实体完整代码
  • 字段逐一分析:pid / userId / textContent / medias / locationName / longitude / latitude / state / created
  • medias 数组嵌入 vs 单独建 media 表的取舍
  • pid 与 _id:业务键 ≠ 主键
  • 接口文档
  • POST /movements:multipart 表单,第三种接口形态
  • 请求参数表 + 请求示例
  • 发布流程概述(细节留给代码实现篇)
  • 原型还原:发动态页面线框图
  • 坑与验证:孤儿文件、文档 16MB 上限、mongosh 样例验证

这里在圈子系列中的位置

圈子的整体表设计(为什么选 MongoDB、四张表怎么分工)在圈子:表设计方案里已经讲过,点赞、评论、喜欢这些互动数据的存储后面互动章节会单独展开。这里只聚焦一件事:动态主表 movement 的字段设计,以及「发动态」这一个接口的定义 。时间线表 movement_timeline、好友表 friend 只在流程图里露个脸,不展开。

movement 集合:动态主表

实体定义

工程实读 mf-model 模块下的 com.mf.model.mongo.Movement,这是一个 MongoDB 实体,用 @Document 声明集合名:

java 复制代码
package com.mf.model.mongo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.List;

//动态详情表
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "movement")
public class Movement implements java.io.Serializable {

    private ObjectId id; //主键id
    //redis实现,使用Mongodb实现
    private Long pid; //Long类型,用于推荐系统的模型(自动增长)
    private Long created; //发布时间
    private Long userId;
    private String textContent; //文字
    private List<String> medias; //媒体数据,图片或小视频 url
    private String longitude; //经度
    private String latitude; //纬度
    private String locationName; //位置名称
    private Integer state = 0;//状态 0:未审(默认),1:通过,2:驳回
}

几个实读细节值得先说清:

  • 没有继承 BasePojo 。项目里部分 Mongo 实体会抽公共字段(created/updated)到父类,Movement 没这么做,created 直接平铺在实体里,取值是毫秒时间戳,由 API 层在保存前手动赋值。
  • 没有 updated 字段。动态发出去之后几乎不改(编辑朋友圈都不支持),所以只留创建时间。
  • 没有用 @Field 注解 。Java 字段名和文档字段名一一对应(textContent 就存成 textContent),不需要改名映射。
  • id 字段没加 @Id,但 Spring Data MongoDB 对名为 id 的 ObjectId 字段会自动识别为主键,落库时若为 null 会自动生成。
  • _class 字段是 Spring Data 自动写入的类型标识,代码里看不到,查询文档时能看到。

字段逐个看设计理由

字段 类型 含义 设计理由
id ObjectId MongoDB 主键 集合默认主键,自动生成,全局唯一
pid Long 发布唯一标识(业务 id) 给推荐系统用的数字自增 id,底层用 MongoDB findAndModify + $inc 操作 sequence 集合保证递增(工程早期注释写「redis实现」,实读 IdWorker 已切到 Mongo 实现)。推荐模型算相似度时需要一个纯数字、有序的标识,ObjectId 那串十六进制不好使
userId Long 发布者 id 关联 user_info 表。存 id 不存昵称头像,用户改昵称不用回刷所有动态
textContent String 文字内容 原型约束 140 字以内,纯文本存储
medias List<String> 媒体地址数组 只存 URL,图片本体在 OSS。0 到 6 个元素,图片和小视频混用同一字段
longitude String 经度 冗余存位置,为「附近的人/同城动态」预留
latitude String 纬度 同上
locationName String 位置名称 客户端定位后传上来的中文描述,如「中国上海市奉贤区人民路445弄」,直接展示用
state Integer 审核状态 默认 0(未审),1 通过,2 驳回。内容审核是异步链路,先落库再审
created Long 发布时间(毫秒) API 层保存前用 System.currentTimeMillis() 赋值

按一条真实文档看更直观(讲义样例,_class 那行是 Spring Data 自动写的类型标识):

json 复制代码
{
    "_id": ObjectId("5e82dc416401952928c211d8"),
    "pid": NumberLong("10064"),
    "userId": NumberLong("6"),
    "textContent": "最悲伤却又是最痛苦的谎言,就是我还好,没有关系。",
    "medias": [
        "https://mf-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1.jpg",
        "https://mf-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1564567349498.jpg",
        "https://mf-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/7/1564567352977.jpg"
    ],
    "longitude": "121.588627",
    "latitude": "30.935781",
    "state": NumberInt("0"),
    "locationName": "中国上海市奉贤区人民路445弄",
    "created": NumberLong("1585634369493"),
    "_class": "com.mf.dubbo.server.pojo.Publish"
}

顺带指个教材里的坑:样例里 _class 写的是 com.mf.dubbo.server.pojo.Publish,那是另一版项目工程的类名残留;这里配套代码的实体是 com.mf.model.mongo.Movement,字段名两者一致,别被类名带偏。

medias 用数组嵌入,而不是单独建 media 表

这是 movement 设计里最值得说的决定。一条动态最多 6 张图,图片和动态是典型的「包含关系」,图片离开动态没有独立意义,也不会被单独查询、单独分页。放到关系型数据库里你可能习惯拆一张 movement_media 表,用外键关联;在 MongoDB 里直接把 URL 数组嵌进文档。

两种方案对比:

维度 数组嵌入(实际方案) 单独建 media 集合
读取一条动态 一次查询,文字图片全拿到 查动态 + 按 movementId 查媒体,两次查询
写入 与动态同文档原子写入 两次写入,需要自己保证一致性
查「包含某张图的动态」 没这需求,无所谓 天然支持
数组膨胀风险 最多 6 个 URL,可控 无
文档大小 每个 URL 约 70 字节,6 个不到 1KB 无

有人会拿 MongoDB 文档 16MB 上限说事,那条限制是针对整个文档的,但这里只存 URL 不存图片本体 ,图片二进制放在 OSS。就算存满 6 张图的 URL 加上正文,离 16MB 还差好几个数量级。嵌入的前提是「子数据小、有界、只随父文档一起读写」,medias 三条全占,所以放心嵌。

这也呼应了体系结构篇说过的原则:嵌入式文档表达包含关系,引用表达关联关系 。图片「属于」动态,嵌;用户「发布」动态但用户是独立实体,只存 userId 引用。

位置信息为什么冗余存三个字段

longitude、latitude、locationName 三个字段一起存,看起来重复,经纬度算一下不就有位置了?反过来才对:经纬度能算出距离,算不出「中国上海市奉贤区人民路445弄」这个给人看的名字,逆地理编码是客户端定位时就已经做好的,直接传上来存住,展示时零成本。

更重要的是,经纬度留在文档里是为后面「附近的人」「同城动态」铺路,MongoDB 支持地理位置索引(2dsphere),建了索引就能按距离检索动态。这里一句点到,检索怎么建索引、怎么算距离,留给附近的人那章。

pid 与 _id:业务键 ≠ 主键

id(ObjectId)是存储层主键,pid 是业务层的发布唯一标识,两套并存。这个套路前面反复出现过:业务需要一个有业务含义、有序、数字化的标识时,单独造一个业务键,别污染主键。pid 的生成在 API 层保存动态前通过 IdWorker(底层 Redis 自增)取号:

java 复制代码
movement.setPid(idWorker.getNextId("movement"));
movement.setCreated(System.currentTimeMillis());
mongoTemplate.save(movement);

为什么推荐系统不直接用 _id?ObjectId 虽然宏观递增,但它是 12 字节的十六进制串,做向量相似度、排序模型输入时不如一个干净的 Long 好处理,而且 pid 语义上是「第几条发布的动态」,数字大小直接反映先后顺序。

接口文档:POST /movements

第三种接口形态

到这篇为止,项目里出现了三种接口形态,值得放一起对照:

形态 代表接口 Content-Type 服务端接收方式
纯 JSON 登录 POST /login(登录篇) application/json @RequestBody 绑定
纯文件 头像上传 POST /users/header(头像篇) multipart/form-data 单个 MultipartFile
JSON 字段 + 文件混合 发动态 POST /movements multipart/form-data 实体对象 + MultipartFile[]

发动态要同时提交文字、位置这些结构化字段和若干张图片文件,于是整个请求走 multipart 表单:文本字段和文件都是表单里的普通 part。注意一个容易踩的点,既然是表单提交,Controller 就不能加 @RequestBody 了 (它只认 JSON 请求体),Spring MVC 会按表单字段名自动绑定到 Movement 对象的同名属性上。

接口定义

  • 请求路径:POST /movements
  • 请求方式:POST,multipart/form-data
  • 请求参数:
参数 类型 必填 说明
textContent String 是 动态文字内容,140 字以内
longitude String 否 经度
latitude String 否 纬度
locationName String 否 位置的中文描述
imageContent MultipartFile\[\] 否 图片文件列表,客户端一次上传多张(或视频),服务端用 MultipartFile[] 接收
  • 响应:发布成功返回状态码 200,响应体为空(ResponseEntity.ok(null));内容为空时抛业务异常,返回统一错误结构。

请求示意

用 Postman 或前端 FormData 发一个典型请求长这样:

http 复制代码
POST /movements HTTP/1.1
Host: mf-server
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="textContent"

今天天气不错
------WebKitFormBoundary
Content-Disposition: form-data; name="longitude"

121.588627
------WebKitFormBoundary
Content-Disposition: form-data; name="latitude"

30.935781
------WebKitFormBoundary
Content-Disposition: form-data; name="locationName"

中国上海市奉贤区人民路445弄
------WebKitFormBoundary
Content-Disposition: form-data; name="imageContent"; filename="a.jpg"
Content-Type: image/jpeg

<二进制图片数据>
------WebKitFormBoundary
Content-Disposition: form-data; name="imageContent"; filename="b.jpg"
Content-Type: image/jpeg

<二进制图片数据>
------WebKitFormBoundary--

多张图片用同名参数 imageContent 重复出现 ,Spring MVC 会把它们收进 MultipartFile[] 数组。前端对应的 FormData 写法是对同一个 key 多次 append("imageContent", file)。

发布流程概述

接口背后的完整链路,为代码实现篇铺垫:
#mermaid-svg-AKTMy4oKEgym3IGW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AKTMy4oKEgym3IGW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AKTMy4oKEgym3IGW .error-icon{fill:#552222;}#mermaid-svg-AKTMy4oKEgym3IGW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AKTMy4oKEgym3IGW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AKTMy4oKEgym3IGW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AKTMy4oKEgym3IGW .marker.cross{stroke:#333333;}#mermaid-svg-AKTMy4oKEgym3IGW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AKTMy4oKEgym3IGW p{margin:0;}#mermaid-svg-AKTMy4oKEgym3IGW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AKTMy4oKEgym3IGW .cluster-label text{fill:#333;}#mermaid-svg-AKTMy4oKEgym3IGW .cluster-label span{color:#333;}#mermaid-svg-AKTMy4oKEgym3IGW .cluster-label span p{background-color:transparent;}#mermaid-svg-AKTMy4oKEgym3IGW .label text,#mermaid-svg-AKTMy4oKEgym3IGW span{fill:#333;color:#333;}#mermaid-svg-AKTMy4oKEgym3IGW .node rect,#mermaid-svg-AKTMy4oKEgym3IGW .node circle,#mermaid-svg-AKTMy4oKEgym3IGW .node ellipse,#mermaid-svg-AKTMy4oKEgym3IGW .node polygon,#mermaid-svg-AKTMy4oKEgym3IGW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AKTMy4oKEgym3IGW .rough-node .label text,#mermaid-svg-AKTMy4oKEgym3IGW .node .label text,#mermaid-svg-AKTMy4oKEgym3IGW .image-shape .label,#mermaid-svg-AKTMy4oKEgym3IGW .icon-shape .label{text-anchor:middle;}#mermaid-svg-AKTMy4oKEgym3IGW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AKTMy4oKEgym3IGW .rough-node .label,#mermaid-svg-AKTMy4oKEgym3IGW .node .label,#mermaid-svg-AKTMy4oKEgym3IGW .image-shape .label,#mermaid-svg-AKTMy4oKEgym3IGW .icon-shape .label{text-align:center;}#mermaid-svg-AKTMy4oKEgym3IGW .node.clickable{cursor:pointer;}#mermaid-svg-AKTMy4oKEgym3IGW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AKTMy4oKEgym3IGW .arrowheadPath{fill:#333333;}#mermaid-svg-AKTMy4oKEgym3IGW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AKTMy4oKEgym3IGW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AKTMy4oKEgym3IGW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKTMy4oKEgym3IGW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AKTMy4oKEgym3IGW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKTMy4oKEgym3IGW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AKTMy4oKEgym3IGW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AKTMy4oKEgym3IGW .cluster text{fill:#333;}#mermaid-svg-AKTMy4oKEgym3IGW .cluster span{color:#333;}#mermaid-svg-AKTMy4oKEgym3IGW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AKTMy4oKEgym3IGW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AKTMy4oKEgym3IGW rect.text{fill:none;stroke-width:0;}#mermaid-svg-AKTMy4oKEgym3IGW .icon-shape,#mermaid-svg-AKTMy4oKEgym3IGW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKTMy4oKEgym3IGW .icon-shape p,#mermaid-svg-AKTMy4oKEgym3IGW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AKTMy4oKEgym3IGW .icon-shape .label rect,#mermaid-svg-AKTMy4oKEgym3IGW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKTMy4oKEgym3IGW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AKTMy4oKEgym3IGW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AKTMy4oKEgym3IGW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端提交表单
Controller 接收 Movement + MultipartFile 数组
校验 textContent 非空
逐张上传 OSS 换取 URL
组装 Movement: userId / medias
API 层: 生成 pid / created
movement 集合
异步写入好友时间线

三步主干:图片先传 OSS 拿 URL → 组装 Movement 对象 → mongoTemplate.save 落库。落库之后还要往每个好友的时间线表里写一条记录,这一步项目里已经改成了 @Async 异步执行,异步改造的前因后果在发布圈子动态:代码优化(异步处理的改造)一篇里展开,这里不重复。

服务端接收侧的核心代码(Controller 签名部分):

java 复制代码
@RestController
@RequestMapping("/movements")
public class MovementController {

    @Autowired
    private MovementService movementService;

    /**
     * 发布动态
     */
    @PostMapping
    public ResponseEntity movements(Movement movement,
                                    MultipartFile imageContent[]) throws IOException {
        movementService.publishMovement(movement, imageContent);
        return ResponseEntity.ok(null);
    }
}

原型还原:发动态页面

从原型(圈子.html)还原发布入口和发布页两屏:

doc 复制代码
+----------------------------------+
|  <- 返回        发动态            |
+----------------------------------+
|                                  |
|  +----------------------------+  |
|  | 请填写动态(140字以内)      |  |
|  |                            |  |
|  |                            |  |
|  +----------------------------+  |
|                                  |
|  [图1] [图2] [图3]               |
|  [图4] [图5] [+]                 |
|                                  |
|  @ 定位                          |
|                                  |
|  [ 上传 ]                        |
|                                  |
+----------------------------------+
doc 复制代码
+----------------------------------+
|  选择照片                 返回    |
+----------------------------------+
|                                  |
|  [img] [img] [img]               |
|  [img] [img] [img]               |
|  [img] [img] [img]               |
|                                  |
|  最多可选6张                      |
|                                  |
+----------------------------------+

要点:

  • 文案取自原型原文:「请填写动态(140字以内)」、「选择照片」、「上传」、「最多可选6张」。
  • 点击九宫格的「+」进入相册选择列表,一次最多可以选择6张照片,超过6张后就不能点击了 ,这个 6 正是服务端 medias 数组长度上限的产品侧来源。
  • 关于字数,原型的说法是「内容不进行字数限制,但是不能超过140字」,听起来矛盾,实际含义是服务端不做强制截断,140 字是前端提示口径。
  • 「定位」控件取到经纬度和地名,对应表里的 longitude / latitude / locationName 三字段。

坑表

坑 说明
前端传参与 medias 的对应 前端对同名 key imageContent 多次 append,服务端才收得到数组;名字对不上,MultipartFile[] 就是 null,循环上传时直接 NPE
文本与图片的事务边界 图片先传 OSS 成功、随后落库失败,OSS 上就留下没人引用的孤儿文件。MongoDB 单文档写入是原子的,但「OSS + MongoDB」跨存储没有事务,只能靠兜底清理或容忍孤儿文件。头像篇已经遇到过同款问题,思路一致
位置信息隐私 经纬度是用户实时位置,属于敏感数据;接口层面这部分参数是选填,客户端要给「不显示位置」的选项,存储和展示都要考虑脱敏(动态页只显示「距离你 20km」这类模糊值)
文档大小上限 单个 MongoDB 文档上限 16MB。medias 只存 URL,大小无忧;但如果哪天有人想直接把图片 Base64 塞进文档,6 张图轻松超限,红线是「媒体只存 URL」
讲义样例的 _class 讲义文档样例里 _class 是旧工程类名 Publish,以这里实体 Movement 为准,字段定义两者一致

验证要点

集合不用手动建,mongoTemplate.save 首次落库时自动创建。想提前验证字段设计,可以手动插一条样例再查回来:

javascript 复制代码
// mongosh
use mf

// 插入一条样例动态
db.movement.insertOne({
  pid: NumberLong("10001"),
  userId: NumberLong("106"),
  textContent: "测试动态",
  medias: [
    "https://mf-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/106/1.jpg"
  ],
  longitude: "121.588627",
  latitude: "30.935781",
  locationName: "中国上海市奉贤区人民路445弄",
  state: NumberInt("0"),
  created: NumberLong("1585634369493")
})

// 查询确认
db.movement.find({ userId: NumberLong("106") }).pretty()

查询结果里 _id 已由 MongoDB 自动补上 ObjectId,_class 也自动写入,这两点正是「主键交给存储层、业务键自己管」的直观体现。

API 速览

接口 方法 参数形态 说明
/movements POST multipart/form-data 发布动态;表单字段绑定 Movement,imageContent 接收多文件;成功返回 200 空体

官方文档

常见误解澄清

误解 澄清
图片存在 MongoDB 里 medias 只存 OSS 的 URL,图片二进制根本不进数据库
medias 该拆成单独的媒体表 媒体与动态是包含关系、数量有界(≤6)、只随动态一起读写,嵌入数组是最优解;拆表反而多一次查询
_id 就是动态 id,够了 推荐系统需要数字有序的 pid,_id 是 ObjectId,两套并存各司其职
发布接口是 JSON 提交 带文件上传,整个请求是 multipart 表单,Controller 不能用 @RequestBody
movement 的 created 来自 BasePojo 实读实体没有继承 BasePojo,created 平铺在实体里、API 层手动赋值
没有审核概念,发出去就能刷到 有 state 字段(0 未审 / 1 通过 / 2 驳回),内容审核是独立异步链路

总结

  • movement 集合是动态主表:pid(业务自增 id,给推荐系统)、textContent、medias(URL 数组)、位置三字段、state 审核状态、created 毫秒时间戳。
  • medias 数组嵌入是这个设计的题眼:子数据小、有界、只随父读写,嵌进文档一次查询拿全;图片只存 URL,16MB 上限形同虚设。
  • 位置三字段冗余存,经纬度为地理索引预留,locationName 为直接展示省掉逆地理编码。
  • POST /movements 是项目里第三种接口形态:multipart 表单同时装文本字段和多文件,字段名 imageContent 对应 MultipartFile[]。
  • 发布链路主干:传 OSS → 组装对象 → 落库;时间线写入已异步化,细节见异步改造篇。

下一篇发布圈子动态:业务流程和代码步骤分析把发布链路拆成一步步的代码动作,Controller、Service、API 三层怎么协作,那里见。

相关推荐
paopaokaka_luck2 小时前
智慧电玩店管理系统(空闲机台随机分配、游玩时长概率模拟、积分自动结算、积分兑换闭环、 ECharts经营分析、 腾讯地图选址)
java·人工智能·mysql·echarts·推荐算法
Brilliantwxx2 小时前
【STM32】 独立看门狗 IWDG 深度详解:从硬件结构到工程实战
数据库·stm32·单片机·嵌入式硬件·mongodb
运维小斌2 小时前
麒麟 V10 部署 OceanBase:故障速查与验收清单
linux·数据库·oceanbase·麒麟
LMT流氓兔2 小时前
第五单元——第9课:防止同一次操作重复执行
java·服务器·数据库·算法·ai·langchain
OxYGC2 小时前
Spring 面经系列 · 第 6 篇:Spring MVC 请求全流程、父子容器与零配置原理
java·spring·mvc
code2cat2 小时前
Java高级篇之JDBC:连接数据库,完成查询、更新与事务
java·开发语言·数据库
*长铗归来*2 小时前
MySQL新学知识(三)MySQL 事件(EVENT)
数据库·mysql
bksczm2 小时前
Redis数据结构之string
java·redis·mybatis
cc_瀚海知行3 小时前
资料检索 1|直接放入全文、按需读文件和先检索,有什么区别?
java·人工智能·ai