【项目实战】从 0 到 1 构建智能协同云图库(七):空间模块需求与方案设计
目录
三、 深度解析:创建私有空间为何必须采用“加锁 + 事务”模式?
一、需求分析
为进一步提升图库系统的应用价值,项目引入了“空间”模块,允许用户创建私有的图片云盘或个人相册。以下是该模块的核心需求与底层架构设计方案:
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:普通用户编辑专用,仅包含id和spaceName,确保用户绝对无法通过抓包篡改空间级别和容量。 -
SpaceUpdateRequest:管理员更新专用,包含级别、限额等所有可修改字段。
-
-
视图对象 (SpaceVO):封装安全、脱敏的展示数据,内置
objToVo和voToObj静态方法便于快速转换。
-
-
空间级别与限额管理 (Enum):
-
引入
SpaceLevelEnum枚举,统一定义各版本空间(普通版 100MB、专业版 1GB、旗舰版 10GB)的默认容量与数量上限。 -
扩展建议:未来可将这些硬编码限额抽离至外部 JSON 文件或 Nacos 配置中心,无需重新发版即可动态调整各级别额度。
-
2. 空间服务核心逻辑与接口权限
在 SpaceService 和 SpaceController 中,重点实现了数据的自动化处理与严格的接口权限控制。
-
核心 Service 方法封装:
-
validSpace(数据校验):引入布尔参数add区分操作。创建时强制校验名称和级别为空;更新时仅校验传入的新值是否合法(如名称长度)。 -
fillSpaceBySpaceLevel(限额自动填充):创建/更新时,若请求未显式指定限额,系统会自动从枚举类中读取并补全maxSize和maxCount。若管理员已单独指定限额,则以指定值为准,保留了为特定空间“开小灶”的灵活性。
-
-
Controller 接口权限严控:
-
创建空间:所有认证用户均可使用。
-
编辑空间:仅限空间创建者本人使用(结合
SpaceEditRequest只能改名)。 -
更新空间:仅限管理员(通过
@AuthCheck注解拦截)。 -
删除空间:仅允许空间创建人或管理员操作。
-
3. 创建私有空间(防并发与事务控制机制)
核心需求:每个用户最多只能创建一个私有空间。
技术难点:防止用户通过高并发请求恶意创建多个空间。系统未单纯依赖数据库唯一索引,而是采用加锁 + 编程式事务的组合方案。
-
执行步骤与防并发逻辑:
-
参数与权限校验:校验参数,非管理员仅能创建普通版空间。
-
细粒度加锁:以用户的
userId作为锁对象(通过String.intern()或ConcurrentHashMap维护本地锁)。这确保了同一用户同一时刻只能串行执行创建逻辑,且不影响其他用户的并发请求,对性能影响低。 -
开启编程式事务:在锁的代码块内部,使用 Spring 的
transactionTemplate.execute管理事务。-
避坑要点:坚决不用
@Transactional注解,因为注解控制的事务提交可能发生在锁释放之后,依然存在并发漏洞。必须保证事务的提交完全在加锁的范围内。
-
-
查重与落库:在事务内,先查询数据库判断是否已有该用户的空间,若有则报错回滚;若无则执行插入。
-
4. 图片操作的权限控制与数据隔离
图片在上传、编辑、删除和查询时,必须严格区分是“公共图库”还是“私有空间”,并进行隔离。
-
上传与更新逻辑:
-
空间校验:获取
spaceId,如果不为空则校验该空间是否存在。 -
上传鉴权与动态路由:校验当前用户是否为空间的创建者。通过后,根据
spaceId将图片存入不同的云端物理目录(public/{userId}或space/{spaceId})。 -
更新防篡改校验:更新图片时,强制比对请求传递的
spaceId必须与数据库中原图片的spaceId保持一致,防止图片被非法转移。
-
-
删除与编辑逻辑(抽取通用鉴权方法
checkPictureAuth):-
公共图库 (
spaceId == null):仅图片上传者本人或管理员可操作。 -
私有空间 (
spaceId != null):仅该空间的创建者可操作。
-
-
查询隔离(动态条件过滤):
-
查公共图库:
spaceId为空时,强制追加“仅展示已审核通过”的过滤条件。 -
查私有空间:
spaceId不为空时,先校验用户是否为该空间所有者;若是,则无视审核状态直接放行返回全部图片。
-
5. 空间配额管理(业务妥协与性能优化)
核心需求:上传图片占用额度,删除图片释放额度。
技术痛点:若在上传时精准计算大小并加重量级锁,会极大地拖慢上传性能,导致系统卡顿。
业务优化思路:利用“业务设计的巧妙”节约性能开销。限制单张图片最大体积(如 2MB),放弃重量级锁,采用轻量级校验 + 事务更新策略,容忍极端并发下的微小透支,后续通过定时任务修正误差。
-
上传时(校验与扣减):
-
轻量前置校验:保存数据前直接判断
已用数量 >= 最大数量或已用容量 >= 最大容量,满足则直接报错拦截。 -
编程式事务:使用
transactionTemplate开启事务。 -
落库与扣减:插入图片记录;通过 SQL(
set totalSize = totalSize + 图片大小,totalCount = totalCount + 1)更新空间表,随后提交事务。
-
-
删除时(释放与清理):
-
权限校验:调用通用的
checkPictureAuth进行拦截。 -
编程式事务:开启事务。
-
标记与释放:逻辑删除图片记录;通过 SQL(
set totalSize = totalSize - 图片大小,totalCount = totalCount - 1)恢复空间表的额度,提交事务。 -
异步清理文件:事务提交后,调用
@Async异步方法清理云存储上的实际文件(云端清理失败不影响主流程数据一致性)。
-
三、 深度解析:创建私有空间为何必须采用“加锁 + 事务”模式?
在开发“创建私有空间”功能时,我们面临一个严苛的业务规则:每个用户最多只能创建一个私有空间。
如果在单线程环境下,这很简单:先去数据库查一下,没有就插入,有就报错。但放到真实的高并发场景下(例如用户利用脚本瞬间发送多个创建请求),如果只使用常规的数据库事务,就会产生严重的并发漏洞。
为了解决这个问题,我们必须引入“加锁 + 编程式事务”的组合模式。以下是深度解析:
1. 为什么“只有事务,没有锁”行不通?
通常,数据库级别的事务隔离级别(如读已提交或可重复读)只能防止“脏写”或部分“不可重复读”,但无法解决这种“先检查后执行(Check-Then-Act)”的业务逻辑漏洞。
漏洞场景重现:
假设用户瞬间并发发送了请求 A 和请求 B。
-
A 和 B 同时开启了数据库事务。
-
A 和 B 同时去执行查询操作:
查询数据库是否存在该用户的空间。 -
此时数据库都是空的,因此 A 和 B 的查询结果都是“无空间”。
-
随后,A 和 B 同时向下执行插入操作并提交事务。
最终结果: 数据库里成功插入了 2 条该用户的空间记录,业务规则被彻底打破。
2. 为什么“先事务,后加锁”是致命陷阱?
很多人意识到并发问题后,会加上 synchronized 锁,但往往会犯一个非常经典的错误:将事务包裹在锁的外面(比如在带有 @Transactional 注解的方法内部使用 synchronized)。
Java
// ❌ 经典的错误示范 !!!
@Transactional // 1. 开启事务
public void addSpace() {
synchronized (lock) { // 2. 获取锁
// 3. 查数据库:是否存在该用户的空间?
// 4. 不存在则写入数据
} // 5. 释放锁
} // 6. 提交事务(AOP代理在方法结束后执行)
为什么这种写法依然防不住并发?
在 Spring 框架中,@Transactional 是基于 AOP(动态代理)实现的。这就意味着,事务的提交动作,是在整个方法执行完毕后,由外层的代理对象去执行的。
漏洞场景重现:
-
请求 A 进入方法,开启事务,拿到锁。执行完查询和写入后,释放了锁。注意!此时方法还没结束,A 的事务还没提交!
-
此时,外面排队等待的请求 B 瞬间拿到了锁,进入代码块执行查询。
-
因为 A 的事务还没提交到数据库,对于 B 来说,数据库里依然是空的!所以 B 查到的结果还是“无空间”。
-
于是 B 也欢快地执行了插入操作。最后 A 和 B 各自提交了事务。
最终结果: 依然创建了 2 个空间,加的锁形同虚设。
3. 正确方案:“先加锁,后事务”
为了打破上述陷阱,我们得出一个黄金铁律:必须保证事务的开启、提交和回滚,完全被包裹在锁的作用范围之内。
这就是教程中强烈建议放弃 @Transactional 注解,改用 transactionTemplate(编程式事务)的原因。
Java
// ✅ 正确示范:加锁 + 编程式事务
// 1. 先加锁 (控制并发)
synchronized (lock) {
// 2. 在锁的内部,手动开启事务 (保证数据操作的完整性)
transactionTemplate.execute(status -> {
// 3. 查数据库:是否存在该用户的空间?
// 4. 不存在则写入数据
// 5. 提交事务 (这步执行完才会跳出 execute 方法)
});
} // 6. 事务彻底提交后,最后释放锁
完美防御并发的流程:
-
加上
synchronized(userId)后,请求 A 拿到锁,进入代码块。 -
A 手动开启事务 -> 查询(无空间) -> 插入 -> 提交事务到数据库 -> 退出
execute代码块。 -
A 彻底完成了所有动作,最后释放了锁。
-
此时,一直被堵在门外的请求 B 终于拿到了锁,开启它自己的事务。
-
B 去查询数据库,因为 A 之前已经把事务提交了,所以 B 这次赫然发现:“已经存在空间”。
-
B 抛出业务异常,成功阻止了重复创建。
总结: 锁(synchronized)负责控制并发,让请求排队进入;事务(transactionTemplate)负责保证这组读写操作的原子性和一致性。先加锁,在锁内执行完整事务,才是解决此类高并发下创建唯一资源的终极方案。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)