← 中文资源

简体中文版本

主数据

主数据是客户、产品、供应商等跨系统共享、缓慢变化的核心实体,多个系统必须对其保持一致的理解。

阅读英文原文

工作原理

主数据管理跨源识别实体,匹配指向同一现实对象的记录,按存活规则合并,并分配持久标识符。中心枢纽可以拥有黄金记录并发布给消费者;注册表模式则让源系统保持权威,提供交叉引用。人工数据管家处理自动匹配无法解决的例外。

这项工作部分是技术,主要是组织。匹配需要确定性键以及针对名称和地址的模糊规则;存活需要规定每个属性以哪个源为准;消费者需要在不破坏现有交易的情况下,把本地 ID 映射到主 ID。

何时使用

当多个系统必须报告同一批客户或产品、重复导致计费或合规错误,或并购产生重叠实体时,投资主数据。从一个领域和明确业务案例开始。在没有任何交付之前就启动全企业项目,通常会停滞。

权衡

中心枢纽提高一致性,但增加延迟、耦合和新的故障域。注册表保留源系统自治,却留下部分不一致。匹配会产生误报和漏报,因此人工管理无法避免。如果治理委员会审查过多,还会拖慢日常变更。

示例与误解

一家银行在零售、信用卡和财富系统中拥有同一客户,姓名为三种拼写、出生日期有两个版本。MDM 在可用时用统一国民 ID 匹配,否则用姓名加地址的模糊匹配,分配当事人 ID,并把源系统键保存在交叉引用表中。报表现在能把一个客户计为一次,同时各系统保留自己的运营记录。

误解是主数据是一次性清洗。新客户、并购和地址变更会持续产生重复,因此匹配和管理必须持续运行。

代码示例

import java.util.List;
import java.util.Objects;

public class RecordClasses {

    public record Point(int x, int y) {
        public Point {
            if (x < 0 || y < 0) {
                throw new IllegalArgumentException(
                    "Coordinates must be non-negative");
            }
        }

        public double distanceTo(Point other) {
            return Math.sqrt(
                Math.pow(x - other.x, 2) +
                Math.pow(y - other.y, 2)
            );
        }
    }

    public record Rectangle(Point topLeft, Point bottomRight) {
        public int width() {
            return bottomRight.x() - topLeft.x();
        }
        public int height() {
            return bottomRight.y() - topLeft.y();
        }
        public int area() {
            return width() * height();
        }
    }

    public record Employee(
        String name,
        String department,
        double salary,
        List<String> skills
    ) {
        public Employee {
            Objects.requireNonNull(name);
            skills = List.copyOf(skills);
        }
    }

    public static void main(String[] args) {
        Point p1 = new Point(0, 0);
        Point p2 = new Point(3, 4);
        System.out.println("Distance: " + p1.distanceTo(p2));

        Rectangle rect = new Rectangle(p1, p2);
        System.out.println("Area: " + rect.area());

        Employee emp = new Employee("Alice", "Engineering", 95000,
            List.of("Java", "Python", "SQL"));
        System.out.println(emp);
    }
}
package main

import (
	"fmt"
	"math/bits"
)

type Bitset []uint64

func NewBitset(size int) Bitset {
	if size < 0 {
		panic("negative bitset size")
	}
	return make(Bitset, (size+63)/64)
}

func (b Bitset) Set(index int) {
	b[index/64] |= uint64(1) << (uint(index) % 64)
}

func (b Bitset) Clear(index int) {
	b[index/64] &^= uint64(1) << (uint(index) % 64)
}

func (b Bitset) Has(index int) bool {
	return b[index/64]&(uint64(1)<<(uint(index)%64)) != 0
}

func (b Bitset) Union(other Bitset) Bitset {
	size := len(b)
	if len(other) > size {
		size = len(other)
	}
	out := make(Bitset, size)
	for i := range out {
		var left, right uint64
		if i < len(b) {
			left = b[i]
		}
		if i < len(other) {
			right = other[i]
		}
		out[i] = left | right
	}
	return out
}

func (b Bitset) Intersect(other Bitset) Bitset {
	size := len(b)
	if len(other) < size {
		size = len(other)
	}
	out := make(Bitset, size)
	for i := range out {
		out[i] = b[i] & other[i]
	}
	return out
}

func (b Bitset) Count() int {
	total := 0
	for _, word := range b {
		total += bits.OnesCount64(word)
	}
	return total
}

func (b Bitset) Slice() []int {
	out := make([]int, 0, b.Count())
	for wordIndex, word := range b {
		for word != 0 {
			bit := bits.TrailingZeros64(word)
			out = append(out, wordIndex*64+bit)
			word &^= uint64(1) << uint(bit)
		}
	}
	return out
}

func main() {
	active := NewBitset(80)
	for _, id := range []int{1, 5, 64, 79} {
		active.Set(id)
	}
	blocked := NewBitset(80)
	blocked.Set(5)
	blocked.Set(9)

	fmt.Println("count:", active.Count())
	fmt.Println("has 64:", active.Has(64), "has 65:", active.Has(65))
	fmt.Println("members:", active.Slice())
	fmt.Println("union count:", active.Union(blocked).Count())
	fmt.Println("intersect:", active.Intersect(blocked).Slice())

	active.Clear(64)
	fmt.Println("after clear:", active.Count())
}