← 中文资源

简体中文版本

软件事务内存(STM)

STM 让线程在冲突时重试的原子事务内读写共享内存,以乐观并发取代细粒度锁,适合短小的内存事务。

阅读英文原文

概念概述

软件事务内存(STM)(Software Transactional Memory)指的是:STM 让线程在冲突时重试的原子事务内读写共享内存,以乐观并发取代细粒度锁,适合短小的内存事务。

从更严格的定义看,Software transactional memory is a concurrency control mechanism in which shared-memory accesses occur inside transactions that commit atomically or abort and retry when a conflict is detected. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。

为什么需要它

It appears in languages such as Haskell and Clojure and in designs where lock discipline is hard to enforce manually. 它与 Happens-Before Relationship、Thread-Local Storage、Asynchronous I/O 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。

常见误解

  • STM eliminates all blocking and contention
  • a transaction can perform arbitrary side effects safely

需要避免的反模式

  • performing irreversible IO inside a transaction that may retry
  • using STM for long transactions that thrash under load

实践要点

  1. Transactions track the locations they read and validate them at commit time, so a conflict causes a retry rather than deadlock, but retries cost CPU under contention.
  2. Side effects such as network calls or file writes must be deferred until after commit, because a retried transaction would otherwise repeat them.
  3. STM is strongest for in-memory data with short transactions; it does not coordinate with a database and so cannot replace the database’s own transaction semantics.

参考来源

https://en.wikipedia.org/wiki/Software_transactional_memory

代码示例

interface VersionedRow {
  readonly key: string;
  readonly value: string;
  readonly startTs: number;
  readonly endTs: number;
}

export class MvccStore {
  private versions: VersionedRow[] = [];
  private clock = 0;

  beginTransaction(): number {
    return ++this.clock;
  }

  write(key: string, value: string, ts: number): void {
    for (const row of this.versions) {
      if (row.key === key && row.endTs === Number.POSITIVE_INFINITY) row.endTs = ts;
    }
    this.versions.push({ key, value, startTs: ts, endTs: Number.POSITIVE_INFINITY });
  }

  read(key: string, snapshotTs: number): string | undefined {
    let found: VersionedRow | undefined;
    for (const row of this.versions) {
      if (row.key !== key) continue;
      if (row.startTs <= snapshotTs && snapshotTs < row.endTs) {
        if (!found || row.startTs > found.startTs) found = row;
      }
    }
    return found?.value;
  }
}
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func seedAndMeasure(ctx context.Context, client *redis.Client) error {
	keys := []string{"metric:hits", "metric:misses", "metric:errors"}

	// 独立命令走管道:一次网络往返
	pipe := client.Pipeline()
	incrCmds := make([]*redis.IntCmd, len(keys))
	for i, key := range keys {
		incrCmds[i] = pipe.Incr(ctx, key)
	}
	if _, err := pipe.Exec(ctx); err != nil {
		return fmt.Errorf("pipeline exec: %w", err)
	}

	// 事务管道:命令打包进 MULTI/EXEC 原子执行
	tx := client.TxPipeline()
	expireCmds := make([]*redis.BoolCmd, len(keys))
	for i, key := range keys {
		expireCmds[i] = tx.Expire(ctx, key, time.Hour)
	}
	if _, err := tx.Exec(ctx); err != nil {
		return fmt.Errorf("tx exec: %w", err)
	}

	for i, key := range keys {
		fmt.Printf("%s=%d expires=%v\n",
			key, incrCmds[i].Val(), expireCmds[i].Val())
	}
	return nil
}

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:        "127.0.0.1:6379",
		PoolSize:    10,
		ReadTimeout: 500 * time.Millisecond,
	})
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	if err := seedAndMeasure(ctx, client); err != nil {
		fmt.Println("failed:", err)
	}
}