← 中文资源

简体中文版本

数据契约

数据契约是数据生产者与消费者之间可执行的约定,覆盖模式、语义、质量、新鲜度和变更管理。

阅读英文原文

工作原理

契约包含三个层次。结构层定义模式和类型;语义层定义含义,例如 revenue 包含什么、使用什么货币、时间戳是否为 UTC、null 代表什么;运营层定义服务水平,包括新鲜度、完整性、唯一性和事件联系人。好的契约还会说明如何宣布变更,以及消费者能提前多久得到通知。

强制执行把文档变成保证。生产者在发布前按契约校验输出;CI 对上一版本运行兼容性检查;平台监控新鲜度和数据量,并在承诺未兑现时通知负责人。消费者因此可以针对稳定接口构建,而不是逆向工程文件。

何时使用

对跨团队边界、支撑财务或监管报表、或驱动共享看板的数据集使用契约。从影响最大的数据集开始,而不是每张表都上契约。契约最有价值的地方,是静默变更代价高昂,且生产者确实能够被要求兑现承诺。

权衡

契约增加评审和协调开销,可能拖慢需要快速行动的生产者。字段规定过细会让演进痛苦,规定过粗又让消费者暴露在风险中。契约必须由有权限执行的人拥有,否则会变成过时文档。只有模式的契约会漏掉造成大多数事故的语义失败。

示例与误解

事件团队承诺 order_created 中 order_id 唯一、amount 以最小货币单位表示、时间戳为 UTC、五分钟内送达。若生产者改为十进制单位,契约测试会在部署前失败,消费者不会看到被破坏的收入数据。

误解是订阅模式注册表就等于契约。注册表检查结构;它不承诺新鲜度、含义,也不承诺数据停止时有人响应。

代码示例

-- 每个邮箱保留 created_at 最新的一行,标记其余为重复
WITH ranked AS (
  SELECT
    id,
    email,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY email
      ORDER BY created_at DESC, id DESC
    ) AS rn
  FROM subscribers
)
SELECT id, email, created_at, (rn = 1) AS is_primary
FROM ranked
ORDER BY email, rn;

-- 一次性清扫:删除全部非首行
WITH ranked AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY created_at DESC, id DESC
         ) AS rn
  FROM subscribers
)
DELETE FROM subscribers s
USING ranked r
WHERE s.id = r.id
  AND r.rn > 1;

-- 上游堵漏:唯一索引阻止重复再次写入
CREATE UNIQUE INDEX CONCURRENTLY idx_subscribers_email
  ON subscribers (email);
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())
}