一、技术方案设计背景
在档案系统建设过程中,多部门、多用户并发维护同一档案元数据(如状态、分类、备注)时,极易产生“内容矛盾”。典型场景是:用户A修改了档案状态,用户B同时修改了档案分类,后提交者的数据会直接覆盖前提交者的数据,导致信息丢失。为了零门槛解决此问题,本文采用数据库乐观锁(Optimistic Locking)配合字段级智能合并策略。该方案无需引入复杂的分布式锁中间件,直接利用数据库版本控制机制,配合Java服务层逻辑,实现冲突的自动检测与无损合并。
二、数据库环境搭建
首先需要准备MySQL数据库环境,并创建一张带有版本控制字段的档案表。版本控制字段是乐观锁的核心,用于判断数据在被读取后是否被其他事务修改过。
请在MySQL客户端执行以下SQL脚本,完成基础表结构创建:
```sql
CREATE DATABASE IF NOT EXISTS archive_system DEFAULT CHARSET utf8mb4;
USE archive_system;
CREATE TABLE `archive_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`archive_title` VARCHAR(255) NOT NULL COMMENT '档案标题',
`archive_status` VARCHAR(50) DEFAULT 'DRAFT' COMMENT '档案状态',
`category_code` VARCHAR(50) DEFAULT NULL COMMENT '分类编码',
`remark` TEXT COMMENT '备注信息',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`version` INT(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='档案信息表';
-- 插入一条测试数据
INSERT INTO `archive_info` (`archive_title`, `archive_status`, `category_code`, `remark`, `version`)
VALUES ('2023年度财务审计报告', 'DRAFT', 'FINANCE', '待归档', 0);
```
三、Spring Boot项目依赖配置
使用Spring Boot框架快速构建后端服务。为了简化数据库操作,我们使用Spring Data JPA。请创建一个新的Maven项目,并在pom.xml中引入必要的依赖。确保JDK版本为1.8或以上。
以下是pom.xml中dependencies节点的完整配置:
```xml
org.springframework.boot
spring-boot-starter-web
2.7.14
org.springframework.boot
spring-boot-starter-data-jpa
2.7.14
mysql
mysql-connector-java
8.0.33
org.projectlombok
lombok
1.18.28
provided
```
四、数据源与实体类配置
在src/main/resources/application.yml中配置数据库连接信息。请根据本地实际数据库密码修改password字段。
```yaml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/archive_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none 禁止自动建表,使用我们手动创建的表
show-sql: true 打印SQL语句,方便观察乐观锁逻辑
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
```
接下来创建核心实体类ArchiveInfo.java。关键点在于使用@Version注解标注version字段,JPA会自动处理该字段的递增和校验。
```java
package com.example.archive.entity;
import lombok.Data;
import javax.persistence.;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "archive_info")
public class ArchiveInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String archiveTitle;
private String archiveStatus;
private String categoryCode;
private String remark;
@Column(name = "update_time")
private LocalDateTime updateTime;
@Version // 核心配置:乐观锁版本号字段
private Integer version;
}
```
五、智能合并服务层实现
这是解决“内容矛盾”的核心逻辑。当捕获到乐观锁异常(ObjectOptimisticLockingFailureException)时,说明数据已被他人修改。此时不能直接抛出错误,而应该重新查询数据库最新数据,将本次提交的非空字段合并到最新数据中,然后重试保存。

创建ArchiveService.java:
```java
package com.example.archive.service;
import com.example.archive.entity.ArchiveInfo;
import com.example.archive.repository.ArchiveRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class ArchiveService {
@Resource
private ArchiveRepository archiveRepository;
/
智能更新档案信息
@param requestDto 前端提交的更新数据(可能只包含部分字段)
@return 更新后的数据
/
@Transactional(rollbackFor = Exception.class)
public ArchiveInfo updateArchiveWithMerge(ArchiveInfo requestDto) {
if (requestDto == null || requestDto.getId() == null) {
throw new IllegalArgumentException("参数不合法");
}
// 尝试次数,防止死循环
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
// 1. 先查询数据库中的当前数据
ArchiveInfo dbData = archiveRepository.findById(requestDto.getId())
.orElseThrow(() -> new IllegalArgumentException("档案不存在"));
// 2. 字段级合并策略
// 逻辑:如果前端传来的字段不为空,则覆盖数据库字段;否则保留数据库原有字段
// 注意:这里忽略version字段,由JPA自动处理
if (requestDto.getArchiveTitle() != null) {
dbData.setArchiveTitle(requestDto.getArchiveTitle());
}
if (requestDto.getArchiveStatus() != null) {
dbData.setArchiveStatus(requestDto.getArchiveStatus());
}
if (requestDto.getCategoryCode() != null) {
dbData.setCategoryCode(requestDto.getCategoryCode());
}
if (requestDto.getRemark() != null) {
dbData.setRemark(requestDto.getRemark());
}
// 3. 保存合并后的对象
// JPA会在SQL中自动加上 WHERE version = ? 的判断
return archiveRepository.save(dbData);
} catch (ObjectOptimisticLockingFailureException ex) {
// 捕获乐观锁异常,说明在读取和保存之间,数据被修改了
System.out.println("检测到并发冲突,正在进行第 " + (i + 1) + " 次重试合并...");
if (i == maxRetries - 1) {
throw new RuntimeException("系统繁忙,并发冲突过多,请稍后重试");
}
// 循环继续,进入下一次重试
}
}
return null;
}
}
```
同时需要定义Repository接口ArchiveRepository.java:
```java
package com.example.archive.repository;
import com.example.archive.entity.ArchiveInfo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ArchiveRepository extends JpaRepository
{
}
```
六、接口层开发与测试
创建Controller层接口,供外部调用。为了简化测试,我们直接通过接口接收JSON参数。
创建ArchiveController.java:
```java
package com.example.archive.controller;
import com.example.archive.entity.ArchiveInfo;
import com.example.archive.service.ArchiveService;
import org.springframework.web.bind.annotation.;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/archive")
public class ArchiveController {
@Resource
private ArchiveService archiveService;
/
更新档案接口
/
@PostMapping("/update")
public String updateArchive(@RequestBody ArchiveInfo archiveInfo) {
try {
ArchiveInfo result = archiveService.updateArchiveWithMerge(archiveInfo);
return "更新成功!当前版本号:" + result.getVersion();
} catch (Exception e) {
return "更新失败:" + e.getMessage();
}
}
}
```
七、并发冲突验证测试
启动Spring Boot项目后,我们使用curl命令模拟两个用户同时修改同一条记录,验证“内容矛盾”是否被解决。
场景模拟:数据库中ID为1的记录,初始version为0。
- 用户A操作:只修改
archiveStatus为“APPROVED”。
- 用户B操作:只修改
categoryCode为“FINANCE_2023”。
在终端中执行以下两条命令(几乎同时执行,利用&符号让后台运行):
```bash
用户A请求:修改状态
curl -X POST http://localhost:8080/api/archive/update \
-H "Content-Type: application/json" \
-d '{"id": 1, "archiveStatus": "APPROVED"}' &
用户B请求:修改分类
curl -X POST http://localhost:8080/api/archive/update \
-H "Content-Type: application/json" \
-d '{"id": 1, "categoryCode": "FINANCE_2023"}' &
```
预期结果分析:
- 假设用户A先执行,读取version=0。用户B后执行,也读取version=0。
- 用户A提交更新,SQL执行
UPDATE ... SET version=1 WHERE id=1 AND version=0,成功。数据库version变为1。
- 用户B提交更新,SQL执行
UPDATE ... SET version=1 WHERE id=1 AND version=0,失败,因为version已经是1了。
- 后端捕获
ObjectOptimisticLockingFailureException,进入updateArchiveWithMerge的catch块。
- 代码进行重试:重新查询ID=1的数据(此时version=1,状态是APPROVED)。
- 将用户B的
categoryCode合并进去。
- 再次保存,SQL执行
UPDATE ... SET category_code='FINANCE_2023', version=2 WHERE id=1 AND version=1,成功。
最终,数据库中ID为1的记录将同时包含用户A修改的状态和用户B修改的分类,version变为2。这证明了我们成功解决了档案建设中的内容矛盾,实现了多用户协同编辑的数据一致性保护。