← 中文资源

简体中文版本

批处理与流处理

批处理对有界数据运行定时作业,流处理持续处理无界事件;延迟、成本和正确性要求决定如何选择。

阅读英文原文

工作原理

批处理作业读取有限数据集,通常是文件分区或数据库快照,处理后提交输出。它可以对全部输入排序、连接和重试,并按固定节奏运行。流作业从日志或队列读取事件,为窗口和连接维护状态,并在记录到达时输出结果。它必须处理乱序事件、重启和背压,同时不能丢失进度。

同一种计算常常可以用两种模型表达。每日收入报表可以扫描所有订单;流式版本则维护每分钟的累计值,并在迟到事件出现时修正。

何时分别使用

当分钟或小时级延迟可以接受、输入已经存储,或完整重算是最简单的正确答案时,使用批处理。当决策依赖新鲜事件时使用流处理:欺诈检查、实时看板、告警或运营响应。许多团队同时运行流路径保证新鲜度,再以批路径作为对账的真相来源。

权衡

批处理更容易推理,处理大量数据更便宜,但结果按设计就是过时的。流处理延迟低,却需要状态管理、水位线、精确一次或幂等接收端、延迟监控以及重放策略。在低流量下,流系统通常更贵,因为资源常驻。

示例与误解

支付团队流式处理授权请求,在几秒内拦截可疑卡片,然后运行夜间批处理重新计算已结算收入并发现差异。两条路径共享业务规则,但不共享执行方式。

常见误解是流处理默认就是精确一次。投递保证取决于源、状态后端和接收端。如果接收端不幂等或没有事务,重复处理会重复计数。批处理在作业重跑而没有原子替换输出时,也有类似风险。

代码示例

from collections.abc import Iterator
from typing import Generic, TypeVar

T = TypeVar("T")


class RingBuffer(Generic[T]):
    def __init__(self, capacity: int) -> None:
        if capacity < 1:
            raise ValueError("capacity must be positive")
        self._slots: list[T | None] = [None] * capacity
        self._head = 0
        self._count = 0

    @property
    def capacity(self) -> int:
        return len(self._slots)

    def __len__(self) -> int:
        return self._count

    def push(self, item: T) -> T | None:
        evicted: T | None = None
        if self._count == self.capacity:
            evicted = self._slots[self._head]
            self._head = (self._head + 1) % self.capacity
            index = (self._head + self._count - 1) % self.capacity
        else:
            index = (self._head + self._count) % self.capacity
        self._slots[index] = item
        if self._count < self.capacity:
            self._count += 1
        return evicted

    def items(self) -> Iterator[T]:
        for offset in range(self._count):
            value = self._slots[(self._head + offset) % self.capacity]
            if value is not None:
                yield value

    def latest(self, count: int) -> list[T]:
        if count < 0:
            raise ValueError("count must not be negative")
        if count == 0:
            return []
        return list(self.items())[-count:]


def main() -> None:
    buffer: RingBuffer[str] = RingBuffer(3)
    for event in ["a", "b", "c"]:
        print("push", event, "evicted", buffer.push(event))
    print("full:", list(buffer.items()))
    print("push d evicted:", buffer.push("d"))
    print("after:", list(buffer.items()))
    print("latest 2:", buffer.latest(2))
    print("length:", len(buffer))


if __name__ == "__main__":
    main()
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())
}