考虑至少要实现一个简单的并发控制吧亲!
使用的架构:
- Java 21
- Redis
- MySQL
使用到的包:
- Hutool SecureUtil MD5
- Java Spring Framework MutilPartFile/TransactionTemplate
- Redission Lock
使用的协议:
- HTTP 协议
对于一个使用最基础的 HTTP 协议的文件传输功能。使用 Spring 框架的 Controller / Service / Dal 是可以的。
- 基础的 @PostMapping(“/upload”) 路由即可。
- Service 层处理存储逻辑
- 基于 Redisson 的并发控制
- MD5 算法校验文件完整性
- (预拓展分片、流式传输、预签名 url)
- 调用 dal 写入文件明细
- 逻辑目录,存储文件的实际物理地址
在数据库建表的时候,不仅逻辑上使用 ID 作为主键索引,同时要使用 MD5 作为唯一索引,因为在大多数情况下,我们使用 MD5 进行查询一个文件是否存在于我们的数据空间中。
基础上传业务逻辑代码
在具体的实现流程上,具体发生的情况如下:
- 将接收到的文件进行 MD5 校验计算
- 尝试使用该 MD5 值获取 RLock 锁
- 如果没能获取锁,说明该文件正在被其他线程处理,抛出异常;
- 如果获取了锁,启动 TransactionTemplate 事务调用执行函数,用 status 控制事务的状态。
- 持有锁状态,未开启事务:
- 调用 FileSpaceService 层的方法,进行刷盘、写盘操作,文件成功落盘后进行下一步。(如你所见,这是潜在的最耗时的流程。线程阻塞在这里等待执行完毕。未来这是一个优化点)。
- 如果出现任何异常,调用 Hutool 清除僵尸文件,抛出异常,不进行下一步。
- 持有锁状态,开启事务:
- 调用另外一个 Service 层的方法,进行数据库明细的写入
- 如果发生任何异常,则进行 catch 和回滚,并清除原本写盘的文件。
- 处理上传中断情况,主动设置线程行为;
- 事务提交。
- 最后释放锁。
文件夹嵌套设计
引入另一张表管理文件夹与文件之间的关联关系。
关于一些细节决策……
Q1: 为什么刷盘的行为要放在外面呢?
- 为了避免长事务问题。事务长期持有某条数据库的连接池连接,在高并发情况下可能会导致性能瓶颈的并发问题。在我们的情景下,我们为了保证文件的落实和操作建议等待文件落盘后才写入数据库,如果从头到尾都开启事务,就导致了连接占用的无效等待。
Q2: 事务一定会持有数据库连接池的某条连接吗?
- 事务是基于数据库连接之上的。
Q3: 关于其中的事务传播行为?
- 在我们的情境下,事务方法调用了另一层 Service 中的方法。在 TransactionTemplate 中,默认的事务传播行为是 REQUIRED,意为如果当前已有事务,则加入该事务;若无事务,则以非事务方式执行。
- 不过可喜的是,我们内层的目前还不涉及到事务。因为它目前只被这个方法调用。如果未来要更加通用的话,我们还要作事务传播行为的专门控制。
- 未来我们会开一个小专题讨论这个事务传播行为控制问题。
事务传递