本文目录导读:

这是一个非常典型的企业级数据治理场景,在Java技术栈中,主数据管理(MDM, Master Data Management)通常不只是一个简单的CRUD应用,而是一个涉及数据模型治理、数据标准化、数据同步、版本控制及缓存策略的复杂系统。
下面我将构建一个“客户主数据(Customer Master)” 的案例,涵盖从模型设计到代码实现的核心逻辑。
案例背景
假设公司有多个业务系统(CRM、ERP、BI),需要对“客户”这同一个实体进行统一管理,需求是:
- 唯一索引:通过身份证号/统一社会信用代码保证唯一。
- 数据编码:自动生成全局唯一的“客户编码”(格式:CUST-202405-XXXX)。
- 数据版本:每次修改都会生成一个新版本,支持数据血缘追溯。
- 数据分发:修改后通过消息队列通知下游系统(如ERP)。
数据模型设计 (Entity)
我们使用 JPA / Hibernate 来定义主数据模型。
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "mdm_customer",
uniqueConstraints = @UniqueConstraint(columnNames = "idCard", name = "uk_id_card"))
public class CustomerMaster {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 全局唯一编码,由系统生成
@Column(name = "customer_code", nullable = false, unique = true, length = 32)
private String customerCode;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "id_card", nullable = false, length = 18)
private String idCard; // 身份证号作为唯一标识
@Column(name = "phone", length = 20)
private String phone;
@Column(name = "email", length = 100)
private String email;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private MasterDataStatus status; // ACTIVE, INACTIVE, MERGED
@Column(name = "version_number", nullable = false)
private Integer versionNumber; // 数据版本号
@Column(name = "created_by", length = 50)
private String createdBy;
@Column(name = "created_time", nullable = false, updatable = false)
private LocalDateTime createdTime;
@Column(name = "updated_time")
private LocalDateTime updatedTime;
// 用于记录当前数据是否是最新版本(用于历史追溯)
@Column(name = "is_latest_version")
private Boolean latestVersion;
// Getters and Setters...
}
enum MasterDataStatus {
ACTIVE, INACTIVE, MERGED
}
核心服务层逻辑 (Service)
这是主数据管理的核心,包含了防重、编码生成、版本控制。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
public class CustomerMasterService {
@Autowired
private CustomerMasterRepository repository;
@Autowired
private CustomerCodeGenerator codeGenerator;
/**
* 创建或更新主数据(基于幂等性设计)
* 如果身份证号已存在,则创建新版本并标记旧版本为过期
*/
@Transactional
public CustomerMaster createOrUpdate(CustomerMaster request) {
// 1. 查找是否存在该身份证号的主数据
Optional<CustomerMaster> existingOpt = repository.findByIdCard(request.getIdCard());
if (existingOpt.isPresent()) {
// 存在则进行版本迭代更新
return createNewVersion(existingOpt.get(), request);
} else {
// 不存在则新建
return createNewCustomer(request);
}
}
private CustomerMaster createNewCustomer(CustomerMaster request) {
CustomerMaster entity = new CustomerMaster();
// 生成全局唯一编码
entity.setCustomerCode(codeGenerator.generateCode());
entity.setName(request.getName());
entity.setIdCard(request.getIdCard());
entity.setPhone(request.getPhone());
entity.setEmail(request.getEmail());
entity.setStatus(MasterDataStatus.ACTIVE);
entity.setVersionNumber(1); // 初始版本为1
entity.setCreatedTime(LocalDateTime.now());
entity.setUpdatedTime(LocalDateTime.now());
entity.setLatestVersion(true);
return repository.save(entity);
}
/**
* 版本更新策略
* 将旧记录标记为历史,插入新记录并版本号+1
*/
private CustomerMaster createNewVersion(CustomerMaster oldEntity, CustomerMaster newData) {
// 1. 将旧版本标记为"非最新"
oldEntity.setLatestVersion(false);
repository.save(oldEntity);
// 2. 创建新版本
CustomerMaster newEntity = new CustomerMaster();
// 继承旧的唯一编码
newEntity.setCustomerCode(oldEntity.getCustomerCode());
newEntity.setName(newData.getName() != null ? newData.getName() : oldEntity.getName());
newEntity.setIdCard(oldEntity.getIdCard());
newEntity.setPhone(newData.getPhone() != null ? newData.getPhone() : oldEntity.getPhone());
newEntity.setEmail(newData.getEmail() != null ? newData.getEmail() : oldEntity.getEmail());
// 版本号递增
newEntity.setVersionNumber(oldEntity.getVersionNumber() + 1);
newEntity.setStatus(MasterDataStatus.ACTIVE);
newEntity.setCreatedTime(oldEntity.getCreatedTime()); // 保留原创建时间
newEntity.setUpdatedTime(LocalDateTime.now());
newEntity.setLatestVersion(true);
return repository.save(newEntity);
}
// 获取最新版本(下游系统查询时使用)
public CustomerMaster getLatestByIdCard(String idCard) {
return repository.findByIdCardAndLatestVersionTrue(idCard)
.orElseThrow(() -> new RuntimeException("主数据未找到"));
}
}
数据变更捕获与分发 (Event & MQ)
主数据修改后,需要通知下游系统,使用 Spring Event + RocketMQ/Kafka。
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.kafka.core.KafkaTemplate;
@Component
public class MasterDataEventDispatcher {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
private static final String TOPIC = "MDM_CUSTOMER_CHANGE";
@EventListener
public void handleCustomerChange(CustomerChangeEvent event) {
// 构建数据变更消息体
CustomerChangeMessage message = CustomerChangeMessage.builder()
.customerCode(event.getCustomerMaster().getCustomerCode())
.action(event.getActionType()) // CREATE, UPDATE, DELETE
.dataSnapshot(event.getCustomerMaster())
.build();
// 发送到消息队列
kafkaTemplate.send(TOPIC, message.getCustomerCode(), message);
}
}
缓存策略 (Redis)
主数据访问频率极高,但变更频率较低(相对于业务交易数据),非常适合使用 Redis 旁路缓存。
public class CustomerCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_PREFIX = "MDM:CUST:";
public CustomerMaster getCustomer(String customerCode) {
String key = CACHE_PREFIX + customerCode;
// 1. 查缓存
CustomerMaster cache = (CustomerMaster) redisTemplate.opsForValue().get(key);
if (cache != null) {
return cache;
}
// 2. 查数据库
CustomerMaster db = repository.findByCustomerCode(customerCode);
if (db != null) {
// 3. 设置缓存(TTL 24小时)
redisTemplate.opsForValue().set(key, db, 24, TimeUnit.HOURS);
return db;
}
return null;
}
// 数据更新后,删除缓存
public void evictCache(String customerCode) {
redisTemplate.delete(CACHE_PREFIX + customerCode);
}
}
数据一致性保障(关键点)
在分布式环境下,为了防止主数据混乱,注意以下几点:
- 最终一致性:不允许下游系统直接修改主数据表,必须通过接口调用。
- 使用分布式ID:
CustomerCode可以用雪花算法(Snowflake)或美团Leaf生成,保证全局唯一且有序。 - 异常监控:当主数据因并发产生版本冲突时,使用乐观锁。
// 在实体中添加版本号字段用于乐观锁 @Version private Integer optimisticLockVersion;
总结案例图谱
[CRM系统] ----> [Java MDM Service] ----> [MySQL/MongoDB]
| |
| (版本管理) | (存储主数据)
| |
v v
[Kafka/RabbitMQ] [Redis Cache]
| |
v v
[ERP系统] [BI系统]
这个案例展示了在 Java 中如何构建一个结构化的主数据管理系统,重点在于模型设计、版本迭代、数据分发和缓存一致性,你可以根据业务类型(客户、产品、供应商)复用这个模式。