目录

一、需求分析

1. 核心需求分析

2. 抽象“空间”概念的必要性

3. 数据库表设计

4. 公共图库与私有空间的绝对独立性

二、 空间模块后端开发核心实现

1. 数据模型与基础配置(安全与隔离的基石)

2. 空间服务核心逻辑与接口权限

3. 创建私有空间(防并发与事务控制机制)

4. 图片操作的权限控制与数据隔离

5. 空间配额管理(业务妥协与性能优化)

三、 深度解析:创建私有空间为何必须采用“加锁 + 事务”模式?

1. 为什么“只有事务,没有锁”行不通?

2. 为什么“先事务,后加锁”是致命陷阱?

3. 正确方案:“先加锁,后事务”

一、需求分析

为进一步提升图库系统的应用价值,项目引入了“空间”模块,允许用户创建私有的图片云盘或个人相册。以下是该模块的核心需求与底层架构设计方案:

1. 核心需求分析

空间模块的受众和功能边界划分得非常明确:

  • 用户侧:每位用户最多只能创建一个私有空间。私有空间内的图片具有严格的隐私性(不会展示在公共图库,也无需管理员审核),且受到空间级别与配额的严格限制(如普通版与专业版的容量和数量上限不同,超出后将无法上传)。

  • 管理员侧:具备对整个系统空间的全局管理权限,可以对所有空间进行搜索、编辑和删除操作。

2. 抽象“空间”概念的必要性

在图片表里加一个“仅自己可见”的标识也可以实现隐藏,为什么要专门抽象出一个厚重的“空间”模型?

  • 硬性存储成本管控:与纯文本不同,图片会占用大量真实的云存储资源并产生计费。如果仅做可见性隐藏,很难对用户占用的总体积和图片总数进行统筹限制。引入“空间”概念就像为用户分配了一块专属“硬盘”,可以极其方便地针对整个空间进行容量和数量的限额管理。

  • 高可扩展性设计:引入中间层(空间)将私有逻辑与原有的公共图库完全剥离,减少了对老代码的侵入。同时,这种模型能够天然兼容未来可能开发的“团队共享空间”(涉及复杂的成员管理),让项目更易于维护和扩展。

3. 数据库表设计

为了支撑空间配额与隔离逻辑,底层数据结构进行了如下设计与改造:

  • 新建空间表 (space)

    * 包含空间名称、空间级别(spaceLevel:0-普通, 1-专业, 2-旗舰)、限额上限(maxSize, maxCount)、当前已用量(totalSize, totalCount)以及所属用户等字段。

    * 设计亮点:将“空间级别”与“限额上限”分为独立字段。这样即便用户属于某个标准级别,管理员也能单独为该空间微调容量上限,不完全与级别枚举绑定,灵活性更高。

    * 性能优化:为高频查询字段(如 userId, spaceName, spaceLevel)专门建立了数据库索引。

  • 改造图片表 (picture)

    * 在原有图片表中新增 spaceId 字段(并添加索引),用于关联图片与所属空间。

    * 默认状态约定:默认情况下 spaceId 为空(NULL),代表该图片被上传到了“公共图库”。

4. 公共图库与私有空间的绝对独立性

在系统设计上,并没有为了省事将“公共图库”当作管理员创建的一个“超大公共空间”来处理,而是让两者保持了本质上的相互独立。这种隔离是为了避免逻辑冲突:

  • 权限体系互斥:公共图库是完全开放的,免登录即可任意查看;而私有空间有着极其严格的鉴权边界,仅限空间所有者(或未来的团队成员)访问。

  • 额度与物理存储隔离:公共图库没有容量配额限制,文件直接存放在云端对象存储的开放目录(如 public);而私有空间受限于用户配额,且文件在物理层面上被隔离在独立的受控目录(如 space)中,以便进一步设置目录级别的访问权限。

  • 术语隔离:在系统表述上,严格使用“公共图库”而非“公共空间”,确保各个阶段的业务逻辑互不干扰、清晰明了。

以下为您重新整理的空间模块后端开发核心内容,去除了重复段落,按业务模块将底层逻辑和实现步骤进行了结构化梳理:

这份文本由于多次补充,确实存在部分内容重叠和结构交叉的情况。为了让您在不遗漏任何核心技术细节的前提下,能够更清晰地理解整个“空间模块后端开发”的底层逻辑,我将这部分内容进行了深度去重、重新分类和逻辑梳理。

以下是整理后的全面总结,共分为 5 个核心业务模块

二、 空间模块后端开发核心实现

1. 数据模型与基础配置(安全与隔离的基石)

后端开发的第一步是对数据模型进行极其严格的拆分,从入口处杜绝越权风险,并建立统一的限额标准。

  • 数据模型分层拆分

    • 实体类 (Space):对应底层数据库表,包含空间名、级别、容量/数量限制、当前使用量等字段,配置 @TableLogic 逻辑删除。

    • 请求对象 (DTO):按操作场景严格拆分,实现物理隔离防篡改。

      • SpaceAddRequest:用于创建空间。

      • SpaceEditRequest普通用户编辑专用,仅包含 idspaceName,确保用户绝对无法通过抓包篡改空间级别和容量。

      • SpaceUpdateRequest管理员更新专用,包含级别、限额等所有可修改字段。

    • 视图对象 (SpaceVO):封装安全、脱敏的展示数据,内置 objToVovoToObj 静态方法便于快速转换。

  • 空间级别与限额管理 (Enum)

    • 引入 SpaceLevelEnum 枚举,统一定义各版本空间(普通版 100MB、专业版 1GB、旗舰版 10GB)的默认容量与数量上限。

    • 扩展建议:未来可将这些硬编码限额抽离至外部 JSON 文件或 Nacos 配置中心,无需重新发版即可动态调整各级别额度。

2. 空间服务核心逻辑与接口权限

SpaceServiceSpaceController 中,重点实现了数据的自动化处理与严格的接口权限控制。

  • 核心 Service 方法封装

    • validSpace (数据校验):引入布尔参数 add 区分操作。创建时强制校验名称和级别为空;更新时仅校验传入的新值是否合法(如名称长度)。

    • fillSpaceBySpaceLevel (限额自动填充):创建/更新时,若请求未显式指定限额,系统会自动从枚举类中读取并补全 maxSizemaxCount。若管理员已单独指定限额,则以指定值为准,保留了为特定空间“开小灶”的灵活性。

  • Controller 接口权限严控

    • 创建空间:所有认证用户均可使用。

    • 编辑空间:仅限空间创建者本人使用(结合 SpaceEditRequest 只能改名)。

    • 更新空间仅限管理员(通过 @AuthCheck 注解拦截)。

    • 删除空间:仅允许空间创建人或管理员操作。

3. 创建私有空间(防并发与事务控制机制)

核心需求:每个用户最多只能创建一个私有空间。

技术难点:防止用户通过高并发请求恶意创建多个空间。系统未单纯依赖数据库唯一索引,而是采用加锁 + 编程式事务的组合方案。

  • 执行步骤与防并发逻辑

    1. 参数与权限校验:校验参数,非管理员仅能创建普通版空间。

    2. 细粒度加锁:以用户的 userId 作为锁对象(通过 String.intern()ConcurrentHashMap 维护本地锁)。这确保了同一用户同一时刻只能串行执行创建逻辑,且不影响其他用户的并发请求,对性能影响低。

    3. 开启编程式事务:在锁的代码块内部,使用 Spring 的 transactionTemplate.execute 管理事务。

      • 避坑要点:坚决不用 @Transactional 注解,因为注解控制的事务提交可能发生在锁释放之后,依然存在并发漏洞。必须保证事务的提交完全在加锁的范围内。

    4. 查重与落库:在事务内,先查询数据库判断是否已有该用户的空间,若有则报错回滚;若无则执行插入。

4. 图片操作的权限控制与数据隔离

图片在上传、编辑、删除和查询时,必须严格区分是“公共图库”还是“私有空间”,并进行隔离。

  • 上传与更新逻辑

    • 空间校验:获取 spaceId,如果不为空则校验该空间是否存在。

    • 上传鉴权与动态路由:校验当前用户是否为空间的创建者。通过后,根据 spaceId 将图片存入不同的云端物理目录(public/{userId}space/{spaceId})。

    • 更新防篡改校验:更新图片时,强制比对请求传递的 spaceId 必须与数据库中原图片的 spaceId 保持一致,防止图片被非法转移。

  • 删除与编辑逻辑(抽取通用鉴权方法 checkPictureAuth

    • 公共图库 (spaceId == null):仅图片上传者本人或管理员可操作。

    • 私有空间 (spaceId != null):仅该空间的创建者可操作。

  • 查询隔离(动态条件过滤)

    • 查公共图库spaceId 为空时,强制追加“仅展示已审核通过”的过滤条件。

    • 查私有空间spaceId 不为空时,先校验用户是否为该空间所有者;若是,则无视审核状态直接放行返回全部图片。

5. 空间配额管理(业务妥协与性能优化)

核心需求:上传图片占用额度,删除图片释放额度。

技术痛点:若在上传时精准计算大小并加重量级锁,会极大地拖慢上传性能,导致系统卡顿。

业务优化思路:利用“业务设计的巧妙”节约性能开销。限制单张图片最大体积(如 2MB),放弃重量级锁,采用轻量级校验 + 事务更新策略,容忍极端并发下的微小透支,后续通过定时任务修正误差。

  • 上传时(校验与扣减)

    1. 轻量前置校验:保存数据前直接判断 已用数量 >= 最大数量已用容量 >= 最大容量,满足则直接报错拦截。

    2. 编程式事务:使用 transactionTemplate 开启事务。

    3. 落库与扣减:插入图片记录;通过 SQL(set totalSize = totalSize + 图片大小, totalCount = totalCount + 1)更新空间表,随后提交事务。

  • 删除时(释放与清理)

    1. 权限校验:调用通用的 checkPictureAuth 进行拦截。

    2. 编程式事务:开启事务。

    3. 标记与释放:逻辑删除图片记录;通过 SQL(set totalSize = totalSize - 图片大小, totalCount = totalCount - 1)恢复空间表的额度,提交事务。

    4. 异步清理文件:事务提交后,调用 @Async 异步方法清理云存储上的实际文件(云端清理失败不影响主流程数据一致性)。


三、 深度解析:创建私有空间为何必须采用“加锁 + 事务”模式?

在开发“创建私有空间”功能时,我们面临一个严苛的业务规则:每个用户最多只能创建一个私有空间

如果在单线程环境下,这很简单:先去数据库查一下,没有就插入,有就报错。但放到真实的高并发场景下(例如用户利用脚本瞬间发送多个创建请求),如果只使用常规的数据库事务,就会产生严重的并发漏洞。

为了解决这个问题,我们必须引入“加锁 + 编程式事务”的组合模式。以下是深度解析:

1. 为什么“只有事务,没有锁”行不通?

通常,数据库级别的事务隔离级别(如读已提交或可重复读)只能防止“脏写”或部分“不可重复读”,但无法解决这种“先检查后执行(Check-Then-Act)”的业务逻辑漏洞。

漏洞场景重现:

假设用户瞬间并发发送了请求 A请求 B

  1. A 和 B 同时开启了数据库事务。

  2. A 和 B 同时去执行查询操作:查询数据库是否存在该用户的空间

  3. 此时数据库都是空的,因此 A 和 B 的查询结果都是“无空间”。

  4. 随后,A 和 B 同时向下执行插入操作并提交事务。

    最终结果: 数据库里成功插入了 2 条该用户的空间记录,业务规则被彻底打破。

2. 为什么“先事务,后加锁”是致命陷阱?

很多人意识到并发问题后,会加上 synchronized 锁,但往往会犯一个非常经典的错误:将事务包裹在锁的外面(比如在带有 @Transactional 注解的方法内部使用 synchronized)。

Java

// ❌ 经典的错误示范 !!!
@Transactional // 1. 开启事务
public void addSpace() {
    synchronized (lock) { // 2. 获取锁
        // 3. 查数据库:是否存在该用户的空间?
        // 4. 不存在则写入数据
    } // 5. 释放锁
} // 6. 提交事务(AOP代理在方法结束后执行)

为什么这种写法依然防不住并发?

在 Spring 框架中,@Transactional 是基于 AOP(动态代理)实现的。这就意味着,事务的提交动作,是在整个方法执行完毕后,由外层的代理对象去执行的

漏洞场景重现:

  1. 请求 A 进入方法,开启事务,拿到锁。执行完查询和写入后,释放了锁。注意!此时方法还没结束,A 的事务还没提交!

  2. 此时,外面排队等待的请求 B 瞬间拿到了锁,进入代码块执行查询。

  3. 因为 A 的事务还没提交到数据库,对于 B 来说,数据库里依然是空的!所以 B 查到的结果还是“无空间”。

  4. 于是 B 也欢快地执行了插入操作。最后 A 和 B 各自提交了事务。

    最终结果: 依然创建了 2 个空间,加的锁形同虚设。

3. 正确方案:“先加锁,后事务”

为了打破上述陷阱,我们得出一个黄金铁律:必须保证事务的开启、提交和回滚,完全被包裹在锁的作用范围之内。

这就是教程中强烈建议放弃 @Transactional 注解,改用 transactionTemplate(编程式事务)的原因。

Java

// ✅ 正确示范:加锁 + 编程式事务
// 1. 先加锁 (控制并发)
synchronized (lock) {
    // 2. 在锁的内部,手动开启事务 (保证数据操作的完整性)
    transactionTemplate.execute(status -> {
        // 3. 查数据库:是否存在该用户的空间?
        // 4. 不存在则写入数据
        // 5. 提交事务 (这步执行完才会跳出 execute 方法)
    });
} // 6. 事务彻底提交后,最后释放锁

完美防御并发的流程:

  1. 加上 synchronized(userId) 后,请求 A 拿到锁,进入代码块。

  2. A 手动开启事务 -> 查询(无空间) -> 插入 -> 提交事务到数据库 -> 退出 execute 代码块。

  3. A 彻底完成了所有动作,最后释放了锁

  4. 此时,一直被堵在门外的请求 B 终于拿到了锁,开启它自己的事务。

  5. B 去查询数据库,因为 A 之前已经把事务提交了,所以 B 这次赫然发现:“已经存在空间”

  6. B 抛出业务异常,成功阻止了重复创建。

总结: 锁(synchronized)负责控制并发,让请求排队进入;事务(transactionTemplate)负责保证这组读写操作的原子性和一致性。先加锁,在锁内执行完整事务,才是解决此类高并发下创建唯一资源的终极方案。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐