From d5de65fdb1802cdf498d65d93397f290813c377c Mon Sep 17 00:00:00 2001 From: muqiuhan Date: Tue, 9 Sep 2025 06:17:00 +0000 Subject: deploy: 9665097f0fa0dae9f92123fac54d26c0818758a5 --- .../index.html" | 14 ++-- .../index.html" | 20 ++++- .../index.html" | 26 +++---- .../19/Repository-pattern-in-Typescript/index.html | 16 ++-- .../index.html" | 1 - .../index.html" | 1 - .../index.html" | 6 +- .../index.html" | 6 -- .../index.html" | 14 +++- .../index.html" | 32 +++++--- 2025/03/25/Scala-3-Capture-Checking/index.html | 16 ++-- .../index.html" | 5 +- .../index.html" | 45 ++++++----- .../index.html" | 18 +++-- 2025/04/13/uuidv7-rdbms/index.html | 28 ++++--- 2025/04/20/linux-amd-screen-boom/index.html | 1 - 2025/05/07/nestjs-bullmq-mail-business/index.html | 41 ++++++----- .../index.html | 82 +++++++++++++-------- 2025/05/27/vertical-slicing-practice/index.html | 2 +- 2025/06/30/ibs/index.html | 30 +++++--- 2025/07/08/dadgad/index.html | 5 +- 2025/07/11/fuck-hcho/index.html | 75 +++++++++++-------- 2025/07/11/rep-date/index.html | 14 ++-- 2025/07/29/nestjs-nested-object-dto/index.html | 42 +++++++---- 2025/07/31/svelte-lazyquery/index.html | 35 +++++---- .../index.html | 15 ++-- .../index.html | 86 +++++++++++++++++----- 27 files changed, 411 insertions(+), 265 deletions(-) (limited to '2025') diff --git "a/2025/02/10/\345\256\236\346\227\266\346\220\234\347\264\242\344\270\255\347\232\204\351\230\262\346\212\226\345\207\275\346\225\260/index.html" "b/2025/02/10/\345\256\236\346\227\266\346\220\234\347\264\242\344\270\255\347\232\204\351\230\262\346\212\226\345\207\275\346\225\260/index.html" index 4f9ecd38..2390de3e 100644 --- "a/2025/02/10/\345\256\236\346\227\266\346\220\234\347\264\242\344\270\255\347\232\204\351\230\262\346\212\226\345\207\275\346\225\260/index.html" +++ "b/2025/02/10/\345\256\236\346\227\266\346\220\234\347\264\242\344\270\255\347\232\204\351\230\262\346\212\226\345\207\275\346\225\260/index.html" @@ -193,18 +193,20 @@

在实现实时搜索功能时,通常会使用输入框的事件监听器来捕获用户的输入变化,并在输入变化时发送搜索请求。为了避免过多的请求导致服务器负担过重,通常会使用“防抖”(debounce)技术来控制请求的频率。

-

实现步骤

    +

    实现步骤

    +
    1. 监听输入框的变化:使用input事件监听器来捕获用户的输入变化。
    2. 防抖处理:使用防抖函数来限制请求的频率。防抖函数会在用户停止输入一段时间后才发送请求。
    3. 发送请求:在防抖函数中调用搜索请求。
    -

    防抖函数示例

    以下是一个简单的防抖函数示例:

    +

    防抖函数示例

    +

    以下是一个简单的防抖函数示例:

    1
    2
    3
    4
    5
    6
    7
    8
    function debounce(func, wait) {
    let timeout;
    return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
    };
    }
    - -

    实现实时搜索

    假设你有一个输入框用于搜索患者:

    +

    实现实时搜索

    +

    假设你有一个输入框用于搜索患者:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <script>
    import { onMount } from 'svelte';
    import { patientsStore } from '$lib/stores/patients.svelte';

    let searchTerm = '';

    // 防抖函数
    function debounce(func, wait) {
    let timeout;
    return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
    };
    }

    // 搜索函数
    const searchPatients = debounce(async (term) => {
    if (term) {
    // 发送搜索请求
    const response = await fetch(`/api/search-patients?query=${term}`);
    const data = await response.json();
    patientsStore.mbglPatients = data;
    } else {
    // 清空搜索结果或恢复默认数据
    patientsStore.mbglPatients = [];
    }
    }, 300); // 300ms 的防抖时间

    // 监听输入框变化
    function handleInput(event) {
    searchTerm = event.target.value;
    searchPatients(searchTerm);
    }
    </script>

    <input type="text" placeholder="搜索患者..." on:input={handleInput} bind:value={searchTerm} />
    - -

    请求发送间隔

      +

      请求发送间隔

      +
      • 防抖时间:通常设置为 300ms 到 500ms 之间。这个时间足够让用户完成输入并减少不必要的请求。
      • 考虑用户体验:防抖时间过短可能导致过多请求,过长则可能让用户感到延迟。300ms 是一个常用的折中值。
      diff --git "a/2025/02/11/\344\273\216\344\272\213\344\273\266\351\243\216\346\232\264\347\234\213\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/index.html" "b/2025/02/11/\344\273\216\344\272\213\344\273\266\351\243\216\346\232\264\347\234\213\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/index.html" index 7afb6836..3ef557ea 100644 --- "a/2025/02/11/\344\273\216\344\272\213\344\273\266\351\243\216\346\232\264\347\234\213\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/index.html" +++ "b/2025/02/11/\344\273\216\344\272\213\344\273\266\351\243\216\346\232\264\347\234\213\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/index.html" @@ -194,7 +194,25 @@

      事件风暴(Event Storming)是一种领域驱动设计(DDD)的实践方法,由 Alberto Brandolini 提出,旨在通过团队协作的方式快速理解和建模业务领域。

      以下是事件风暴及相关领域驱动设计中的一些核心概念和知识点:

      -

      1. 领域(Domain):指的是业务相关知识的集合,可以进一步划分为子域。
      2. 子域(Subdomain):是领域的一部分,可以是核心域、支撑域或通用域。
      3. 核心域(Core Domain):指领域中最核心的部分,通常对应企业的核心业务。
      4. 通用语言(Ubiquitous Language):团队所有成员使用的一种语言,用于确保业务和软件之间的沟通一致性。
      5. 限界上下文(Bounded Context):定义了一组规则和协议,用于明确领域模型的适用范围。
      6. 实体(Entity):具有唯一标识和生命周期的领域对象。
      7. 值对象(Value Object):描述了某种特性或属性的对象,没有概念标识。
      8. 聚合(Aggregate):一组相关对象的集合,由一个聚合根(Aggregate Root)统一管理。
      9. 领域事件(Domain Event):领域中发生的重要事件,可以用于通知其他领域对象或跨限界上下文进行解耦和协作。
      10. 命令(Command):表示要执行的操作,通常与事件一一对应。
      11. 读模型(Read Model):为了优化读取操作而设计的模型,可能与写模型不同。
      12. 决策命令(Decision Command):在事件风暴中,直接导致事件发生的命令。
      13. 战略设计(Strategic Design):高层次的抽象和归类,包括理清上下文和子域的划分。
      14. 战术设计(Tactical Design):对特定上下文下的模型进行详细设计,包括聚合、实体和值对象。
      15. 贫血模型(Anemic Domain Model):领域对象只有属性及其getter/setter方法的纯数据类,业务逻辑通过服务实现。
      16. 充血模型(Rich Domain Model):领域对象包含业务逻辑,每个对象都是活跃的。
      17. 资源库(Repository):用于检索和持久化领域对象的机制。
      18. 服务(Service):在模型中独立的操作,可以是领域服务或应用服务。
      19. 固定规则(Invariant):为设计元素做出的断言,必须一直保持为真。

      +

      1. 领域(Domain):指的是业务相关知识的集合,可以进一步划分为子域。
      +2. 子域(Subdomain):是领域的一部分,可以是核心域、支撑域或通用域。
      +3. 核心域(Core Domain):指领域中最核心的部分,通常对应企业的核心业务。
      +4. 通用语言(Ubiquitous Language):团队所有成员使用的一种语言,用于确保业务和软件之间的沟通一致性。
      +5. 限界上下文(Bounded Context):定义了一组规则和协议,用于明确领域模型的适用范围。
      +6. 实体(Entity):具有唯一标识和生命周期的领域对象。
      +7. 值对象(Value Object):描述了某种特性或属性的对象,没有概念标识。
      +8. 聚合(Aggregate):一组相关对象的集合,由一个聚合根(Aggregate Root)统一管理。
      +9. 领域事件(Domain Event):领域中发生的重要事件,可以用于通知其他领域对象或跨限界上下文进行解耦和协作。
      +10. 命令(Command):表示要执行的操作,通常与事件一一对应。
      +11. 读模型(Read Model):为了优化读取操作而设计的模型,可能与写模型不同。
      +12. 决策命令(Decision Command):在事件风暴中,直接导致事件发生的命令。
      +13. 战略设计(Strategic Design):高层次的抽象和归类,包括理清上下文和子域的划分。
      +14. 战术设计(Tactical Design):对特定上下文下的模型进行详细设计,包括聚合、实体和值对象。
      +15. 贫血模型(Anemic Domain Model):领域对象只有属性及其getter/setter方法的纯数据类,业务逻辑通过服务实现。
      +16. 充血模型(Rich Domain Model):领域对象包含业务逻辑,每个对象都是活跃的。
      +17. 资源库(Repository):用于检索和持久化领域对象的机制。
      +18. 服务(Service):在模型中独立的操作,可以是领域服务或应用服务。
      +19. 固定规则(Invariant):为设计元素做出的断言,必须一直保持为真。

      事件风暴通常包括以下步骤:

      • 识别领域事件
      • diff --git "a/2025/02/18/Prisma-\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\347\232\204-Self-relations/index.html" "b/2025/02/18/Prisma-\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\347\232\204-Self-relations/index.html" index 0eac083b..0916a06e 100644 --- "a/2025/02/18/Prisma-\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\347\232\204-Self-relations/index.html" +++ "b/2025/02/18/Prisma-\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\347\232\204-Self-relations/index.html" @@ -195,8 +195,8 @@

        A relation field can also reference its own model, in this case the relation is called a self-relation. Self-relations can be of any cardinality, 1-1, 1-n and m-n.

        -

        一对一

        1
        2
        3
        4
        5
        6
        7
        model User {
        id Int @id @default(autoincrement())
        name String?
        successorId Int? @unique
        successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
        predecessor User? @relation("BlogOwnerHistory")
        }
        - +

        一对一

        +
        1
        2
        3
        4
        5
        6
        7
        model User {
        id Int @id @default(autoincrement())
        name String?
        successorId Int? @unique
        successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
        predecessor User? @relation("BlogOwnerHistory")
        }

        User 展现了这样一个模型:

        • User 可以有一个或零个前驱(predecessor)
        • @@ -214,9 +214,8 @@

          而在关系型数据库中,一对一的 self-relation 可以用如下 SQL 描述:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          CREATE TABLE "User" (
          id SERIAL PRIMARY KEY,
          "name" TEXT,
          "successorId" INTEGER
          );

          ALTER TABLE "User" ADD CONSTRAINT fk_successor_user FOREIGN KEY ("successorId") REFERENCES "User" (id);

          ALTER TABLE "User" ADD CONSTRAINT successor_unique UNIQUE ("successorId");
          - -

          一对多

          1
          2
          3
          4
          5
          6
          7
          model User {
          id Int @id @default(autoincrement())
          name String?
          teacherId Int?
          teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
          students User[] @relation("TeacherStudents")
          }
          - +

          一对多

          +
          1
          2
          3
          4
          5
          6
          7
          model User {
          id Int @id @default(autoincrement())
          name String?
          teacherId Int?
          teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
          students User[] @relation("TeacherStudents")
          }

          User 展现了这样一个模型:

          • 一个 User 只能有零个或一个 teacher
          • @@ -227,27 +226,26 @@

            用 SQL 描述 User model:

            1
            2
            3
            4
            5
            6
            7
            CREATE TABLE "User" (
            id SERIAL PRIMARY KEY,
            "name" TEXT,
            "teacherId" INTEGER
            );

            ALTER TABLE "User" ADD CONSTRAINT fk_teacherid_user FOREIGN KEY ("teacherId") REFERENCES "User" (id);
            -

            teacherId 没有使用 UNIQUE 约束,这代表着多个 students 可以有同一个 teacher

            -

            多对多

            1
            2
            3
            4
            5
            6
            model User {
            id Int @id @default(autoincrement())
            name String?
            followedBy User[] @relation("UserFollows")
            following User[] @relation("UserFollows")
            }
            - +

            多对多

            +
            1
            2
            3
            4
            5
            6
            model User {
            id Int @id @default(autoincrement())
            name String?
            followedBy User[] @relation("UserFollows")
            following User[] @relation("UserFollows")
            }
            • 一个 User 可以被零个或多个 Users 关注
            • 一个 User 可以关注零个或多个 Users
            -

            对于关系型数据库,多对多的关系是隐式的,这意味着 Prisma ORM 会在底层数据库中维护一个 relation table:
            A relation table (also sometimes called a JOIN, link or pivot table) connects two or more other tables and therefore creates a relation between them. Creating relation tables is a common data modelling practice in SQL to represent relationships between different entities. In essence it means that “one m-n relation is modeled as two 1-n relations in the database”.

            +

            对于关系型数据库,多对多的关系是隐式的,这意味着 Prisma ORM 会在底层数据库中维护一个 relation table:
            +A relation table (also sometimes called a JOIN, link or pivot table) connects two or more other tables and therefore creates a relation between them. Creating relation tables is a common data modelling practice in SQL to represent relationships between different entities. In essence it means that “one m-n relation is modeled as two 1-n relations in the database”.

            We recommend using implicit m-n-relations, where Prisma ORM automatically generates the relation table in the underlying database. Explicit m-n-relations should be used when you need to store additional data in the relations, such as the date the relation was created.

            如果需要需要通过多对多的关系来保存其他字段,也可以创建显式的多对多 self 关系:

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15
            model User {
            id Int @id @default(autoincrement())
            name String?
            followedBy Follows[] @relation("followedBy")
            following Follows[] @relation("following")
            }

            model Follows {
            followedBy User @relation("followedBy", fields: [followedById], references: [id])
            followedById Int
            following User @relation("following", fields: [followingId], references: [id])
            followingId Int

            @@id([followingId, followedById])
            }
            -

            在关系型数据库中,可以用如下 SQL 描述:

            1
            2
            3
            4
            5
            6
            7
            8
            CREATE TABLE "User" (
            id integer DEFAULT nextval('"User_id_seq"'::regclass) PRIMARY KEY,
            name text
            );
            CREATE TABLE "_UserFollows" (
            "A" integer NOT NULL REFERENCES "User"(id) ON DELETE CASCADE ON UPDATE CASCADE,
            "B" integer NOT NULL REFERENCES "User"(id) ON DELETE CASCADE ON UPDATE CASCADE
            );
            - -

            在同一模型上建立多个 self-relations

            1
            2
            3
            4
            5
            6
            7
            8
            9
            model User {
            id Int @id @default(autoincrement())
            name String?
            teacherId Int?
            teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
            students User[] @relation("TeacherStudents")
            followedBy User[] @relation("UserFollows")
            following User[] @relation("UserFollows")
            }
            - -

            REFS.

              +

              在同一模型上建立多个 self-relations

              +
              1
              2
              3
              4
              5
              6
              7
              8
              9
              model User {
              id Int @id @default(autoincrement())
              name String?
              teacherId Int?
              teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
              students User[] @relation("TeacherStudents")
              followedBy User[] @relation("UserFollows")
              following User[] @relation("UserFollows")
              }
              +

              REFS.

              +
              • fully annotated
              • required
              • implicit
              • diff --git a/2025/02/19/Repository-pattern-in-Typescript/index.html b/2025/02/19/Repository-pattern-in-Typescript/index.html index c0753a94..99250448 100644 --- a/2025/02/19/Repository-pattern-in-Typescript/index.html +++ b/2025/02/19/Repository-pattern-in-Typescript/index.html @@ -198,30 +198,28 @@

                抽象化数据访问层的主要作用是将应用的业务逻辑与对数据库的数据访问等实现细节进行解耦。DB 框架的更改不应该影响到应用程序的核心服务,并且它们应该对业务逻辑代码透明。

                业务服务应该只依赖于抽象,而不是实现,数据服务同理。


                -

                Intro.

                例如,此时有一个书籍存储的微服务,并且有一个“添加新书籍”的操作用例,在此用例中可以通过使用 Book 这个 Repository 来添加新书籍,而 Book 具体使用什么数据库,怎么存并不是业务需要关心的事情。

                -

                具体实现方式

                假设有以下实体类型定义:

                +

                Intro.

                +

                例如,此时有一个书籍存储的微服务,并且有一个“添加新书籍”的操作用例,在此用例中可以通过使用 Book 这个 Repository 来添加新书籍,而 Book 具体使用什么数据库,怎么存并不是业务需要关心的事情。

                +

                具体实现方式

                +

                假设有以下实体类型定义:

                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                export class Author {
                firstName: string;
                lastName: string;
                }

                export class Genre {
                name: string;
                }

                export class Book {
                title: string;
                author: Author;
                genre: Genre;
                publishDate: Date;
                }
                - -

                抽象化

                第一步是抽象化出一个通用的 Repository ,其中包含通用的操作:

                +

                抽象化

                +

                第一步是抽象化出一个通用的 Repository ,其中包含通用的操作:

                1
                2
                3
                4
                5
                6
                7
                8
                9
                export abstract class IGenericRepository<T> {
                abstract getAll(): Promise<T[]>;

                abstract get(id: string): Promise<T>;

                abstract create(item: T): Promise<T>;

                abstract update(id: string, item: T);
                }
                -
                • 这里具体有那些函数可以根据需要(业务)自己定义。
                • 类型参数 T 表示每个实体。
                -

                接着,定义一个数据服务,其中包含使用 IGenericRepositoiry 定义的具体实例对应的 _Repository_:

                +

                接着,定义一个数据服务,其中包含使用 IGenericRepositoiry 定义的具体实例对应的 Repository:

                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                import { Author, Book, Genre } from '../entities';
                import { IGenericRepository } from './generic-repository.abstract';

                export abstract class IDataServices {
                abstract authors: IGenericRepository<Author>;

                abstract books: IGenericRepository<Book>;

                abstract genres: IGenericRepository<Genre>;
                }
                -
                • 这里有三个 Repository 分别对应不同的实体。
                • IGenericRepository 中定义的函数是每个 Repository 公开的通用存储函数。

                以上这些就是对业务中的实体的 Repository 抽象,这样就隔离开了业务和存储逻辑,例如使用 MongoDB,可以实现一个 MongoGenericRepository:

                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                import { Model } from 'mongoose';
                import { IGenericRepository } from '../../../core';

                export class MongoGenericRepository<T> implements IGenericRepository<T> {
                private _repository: Model<T>;
                private _populateOnFind: string[];

                constructor(repository: Model<T>, populateOnFind: string[] = []) {
                this._repository = repository;
                this._populateOnFind = populateOnFind;
                }

                getAll(): Promise<T[]> {
                return this._repository.find().populate(this._populateOnFind).exec();
                }

                get(id: any): Promise<T> {
                return this._repository.findById(id).populate(this._populateOnFind).exec();
                }

                create(item: T): Promise<T> {
                return this._repository.create(item);
                }

                update(id: string, item: T) {
                return this._repository.findByIdAndUpdate(id, item);
                }
                }
                -

                然后实现一个 MongoDataServices:

                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                33
                34
                35
                36
                37
                38
                39
                40
                import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
                import { InjectModel } from '@nestjs/mongoose';
                import { Model } from 'mongoose';
                import { IDataServices } from '../../../core';
                import { MongoGenericRepository } from './mongo-generic-repository';
                import {
                Author,
                AuthorDocument,
                Book,
                BookDocument,
                Genre,
                GenreDocument,
                } from './model';

                @Injectable()
                export class MongoDataServices
                implements IDataServices, OnApplicationBootstrap
                {
                authors: MongoGenericRepository<Author>;
                books: MongoGenericRepository<Book>;
                genres: MongoGenericRepository<Genre>;

                constructor(
                @InjectModel(Author.name)
                private AuthorRepository: Model<AuthorDocument>,
                @InjectModel(Book.name)
                private BookRepository: Model<BookDocument>,
                @InjectModel(Genre.name)
                private GenreRepository: Model<GenreDocument>,
                ) {}

                onApplicationBootstrap() {
                this.authors = new MongoGenericRepository<Author>(this.AuthorRepository);
                this.books = new MongoGenericRepository<Book>(this.BookRepository, [
                'author',
                'genre',
                ]);
                this.genres = new MongoGenericRepository<Genre>(this.GenreRepository);
                }
                }
                -

                Sunday, March 30, 2025 7:58 PM:

                感觉以上全错,不应该过度抽象 Repository 的,就让它分散在各个模块中,还可以参考 https://github.com/Papooch/nestjs-cls 实现不会抽象泄漏的事务性 Repository 方法。

                diff --git "a/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" "b/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" index 35e8670e..4f96ab6d 100644 --- "a/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" +++ "b/2025/03/13/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\347\232\204\342\200\234\350\201\232\345\220\210\346\240\271\342\200\235/index.html" @@ -207,7 +207,6 @@

              用 F# 来描述,以订单管理为例,大概写一下:

              1
              2
              3
              4
              5
              6
              7
              8
              9
              10
              11
              12
              13
              14
              15
              16
              17
              18
              19
              20
              21
              22
              23
              24
              25
              26
              27
              28
              29
              30
              31
              32
              33
              34
              35
              36
              37
              38
              39
              type OrderStatus = 
              | New
              | Shipped
              | Delivered
              | Cancelled

              type OrderItem (productName: string, price: float, quantity: int) =
              do
              if quantity <= 0 then
              failwith "Quantity must be positive"

              member public this.ProductName = productName
              member public this.Price = price
              member public this.Quantity = quantity
              member public this.TotalPrice () = price * quantity

              type Order (id: int, customerName: string) =
              let mutable status = OrderStatus.New
              let mutable orderItems = []

              member public this.Id = id
              member public this.CustomerName = customerName
              member public this.Status = status
              member public this.OrderItems = orderItems

              member public this.AddItem (item: OrderItem, price: float, quantity: int) =
              if quantity <= 0 then
              failwith "Quantity must be positive"

              orderItems <- orderItems @ [OrderItem(item.ProductName, price, quantity)]

              member public this.ChangeStatus (status: OrderStatus) =
              this.Status <- status

              member public this.TotalPrice () =
              orderItems |> List.sumBy (fun item -> item.TotalPrice())

              member public this.GetTotalPrice () =
              orderItems |> List.sumBy (fun item -> item.TotalPrice())
              -

              在这个例子中,Order 是聚合根,它通过 AddItem 方法来添加订单项,保证每个订单项符合业务规则。同时,聚合根 Order 还负责订单状态的管理,例如通过 ChangeStatus 方法来更新订单状态。OrderItem 是聚合内的一个实体,表示订单项,它通过 GetTotalPrice 方法来计算每个订单项的总价。外部系统只能通过 Order 聚合根来访问和操作订单项,而不能直接访问或修改 OrderItem

      diff --git "a/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" "b/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" index 3f99a42a..d72575f9 100644 --- "a/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" +++ "b/2025/03/14/DDD-\344\270\255\347\232\204-Ubiquitous-Languages/index.html" @@ -209,7 +209,6 @@

假设正在开发一个电子商务系统,业务专家使用“订单”来描述用户购买的商品集合。团队可以在代码中使用“Order”来命名相关的类和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type Order = {
items: OrderItem list
customer: Customer
orderDate: DateTime
}

type OrderItem = {
product: Product
quantity: int
}

type Customer = {
name: string
}

type Product = {
name: string
price: float
}

let addItem (order: Order) (item: OrderItem) =
{ order with items = order.items @ [item] }

let getTotalAmount (order: Order) =
let total = 0.0
for item in order.items do
total <- total + item.price
total

let getPrice (item: OrderItem) =
item.product.price * item.quantity

let getTotalPrice (order: Order) =
let total = 0.0
for item in order.items do
total <- total + getPrice(item)
total
-

在这个示例中使用了“Order”、“OrderItem”、“Customer”和“Product”等术语,这些术语都是 Ubiquitous Language 的一部分,可以提高代码与业务需求的一致性。

diff --git "a/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" "b/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" index 4c617dc1..d4d68cd3 100644 --- "a/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" +++ "b/2025/03/17/\344\272\214\343\200\207\344\272\214\344\272\224\345\271\264\344\270\211\346\234\210\345\215\201\344\270\203\346\227\245/index.html" @@ -301,8 +301,10 @@ mjx-container[display="true"] + br {

它们是因缘聚散吗?

那露珠消散后去了哪里?

此有故彼有,此生故彼灭。

-

当晨光加热露珠表面至 深度时,表层水分子动能突破氢键束缚(键能约 )。
这种相变并非整齐划一的队列解散,而是呈现量子隧穿效应——单个水分子以 量级的涨落,在液态与气态间振荡,直到完全脱离范德华力作用半径(约 )。

-

逃逸的 H2O 分子并非直线升空,而是在空气分子碰撞下进行三维随机游走。
根据爱因斯坦-斯托克斯方程,其扩散系数 (25℃标准大气压)。这意味着单个水分子在1秒内将形成半径约 的概率云,与数十亿同伴共同形成水汽。

+

当晨光加热露珠表面至 深度时,表层水分子动能突破氢键束缚(键能约 )。
+这种相变并非整齐划一的队列解散,而是呈现量子隧穿效应——单个水分子以 量级的涨落,在液态与气态间振荡,直到完全脱离范德华力作用半径(约 )。

+

逃逸的 H2O 分子并非直线升空,而是在空气分子碰撞下进行三维随机游走。
+根据爱因斯坦-斯托克斯方程,其扩散系数 (25℃标准大气压)。这意味着单个水分子在1秒内将形成半径约 的概率云,与数十亿同伴共同形成水汽。

水中捞月,伸手时,涟漪碎了三千世界。

人类的眉睫处有十方虚空,三藏经书不过指月之指,

看见儿时门前溪水倒流,看见婴孩啼哭时眼底星河闪烁。

diff --git "a/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" "b/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" index b15e30ac..f2210beb 100644 --- "a/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" +++ "b/2025/03/19/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\344\270\255\350\201\232\345\220\210\346\240\271\346\214\201\344\271\205\345\214\226\345\222\214\344\272\213\344\273\266\345\217\221\345\270\203\345\217\257\350\203\275\345\257\274\350\207\264\346\225\260\346\215\256\344\270\215\344\270\200\350\207\264\351\227\256\351\242\230/index.html" @@ -194,7 +194,6 @@

使用领域事件的一种直接做法是:在 应用服务 (Application Service) 中产生事件并发布出去。例如,对于“用户昵称更新”的场景来讲,对应的应用服务 UserCommandService 实现如下:

1
2
3
4
5
6
7
8
9
10
member public this.UpdateMyName (command: UpdateUsernameCommand) (user: User) =
let user = userRepository.GetById user.Id
let oldName = user.Username
let newName = command.Username

user.UpdateUsername newName
|> userRepository.Save

UsernameChangeEvent (user.Id, newName, oldName)
|> eventPublisher.Publish
-

这里,在更新了用户姓名之后,即刻调用事件发布器 eventPublisher.Publish 将事件发送到消息队列中。虽然这种方式比较流行,但它至少存在两个问题:

  1. 领域事件本应属于领域模型的一部分,也即应该从领域模型中产生,而这里却在应用服务中产生
  2. @@ -202,24 +201,19 @@

对于第1个问题,可以采用“从领域模型中返回领域事件”的方式:

1
2
3
4
5
6
7
8
9
member public this.UpdateMyName (command: UpdateUsernameCommand) (user: User) =
let user = userRepository.GetById user.Id
let oldName = user.Username
let newName = command.Username

user.UpdateUsername newName // UpdateUsername 中构建 UsernameChangeEvent
|> fun user event ->
userRepository.Save user
eventPublisher.Publish event
-

这种方式保证了领域事件是从领域模型中产生,但仍然存在第二个问题。

第二个问题中所谓的“数据一致性”,表示的是将聚合根保存到数据库和将领域事件发布到消息队列之间的一致性。由于数据库和消息队列属于异构的数据源,要保证他们之间的数据一致性需要引入分布式事务。

但是分布式事务通常是比较重量级的,再加上当下的诸多常见消息队列均不支持分布式事务(比如Kafka),因此并不建议使用分布式事务来解决这个问题。

Transactional Outbox 便是一种方案,概括来说,这种方式将一个分布式事务的问题拆解为多个本地事务,并采用“至少一次投递(At Least Once Delivery)”原则保证消息的发布。具体来讲,发布方在与业务数据相同的数据库中为领域事件创建相应的事件发布表(Outbox table),然后在保存业务数据的同时将所产生的事件保存到事件发布表中,由于此时二者都属于同一个数据库的本地事务所管辖,因此保证了“业务操作”与“事件产生”之间的一致性。此时的代码变成了:

1
2
3
4
5
6
7
8
9
member public this.UpdateMyName (command: UpdateUsernameCommand) (user: User) =
let user = userRepository.GetById user.Id
let oldName = user.Username
let newName = command.Username

user.UpdateUsername newName // UpdateUsername 中构建 UsernameChangeEvent
|> fun user event ->
userRepository.Save user
eventStore.Save event // 这儿用 eventStore 代替 eventPublisher 啦
-

应用服务不再将事件直接发布出去,而是将事件保存到数据库中,之后,另一个模块将从数据库中读取事件并发布。

然而,这种方式依然有个缺点:每个需要产生领域事件的场景都需要应用服务先后调用repository.Save()和eventStore.Save(),导致了代码重复。解决方法也很简单——在聚合根中临时保存领域事件,然后在资源库中同时保存聚合根和领域事件到数据库。

在这种方式下,首先需要在聚合根的基类中完成与领域事件相关的各种设施,包括创建临时性的事件容器events以及通用的事件产生方法RaiseEvent():

1
2
3
4
5
6
7
8
9
[<AbstractClass>]
type IAggregateRoot =
...
let events = Collections.Generic.List<DomainEvent> ()

member private this.RaiseEvent (event: DomainEvent) =
events.Add event

...
-

在聚合根基类AggregateRoot中,events字段用于临时保存聚合根中所产生的所有事件,各实际的聚合根类通过调用RaiseEvent()向events中添加事件。比如,对于“用户修改昵称”而言,User实现如下:

1
2
3
4
5
6
7
8
member public this.UpdateUsername (name: string, user: User) =
if this.Username = name then
()
else
let oldName = this.Username
this.Username <- name
UsernameChangeEvent (user.Id, name, oldName)
|> this.RaiseEvent
-

这里,聚合根 User 不再返回领域事件,而是将领域事件通过AggregateRoot.RaiseEvent()暂时性地保存到自身的events中。之后在保存User时,资源库的公共基类BaseRepository的Save()方法同时完成对聚合根和领域事件的持久化:

1
2
3
4
5
6
7
8
9
member public this.Save<AR: when AR :> AggrateRoot> (it: AR) =
match it with
| null -> failwith "..."
| it when it.Events |> isEmpty |> not ->
this.SaveEvents it.Events
this.CleanEvents ()
| _ -> ()

db.Save it
-

在Save()方法中,首先获取到聚合根中的所有领域事件,然后通过SaveEvents()方法将它们保存到发布事件表中,最后通过db.Save it保存聚合根。需要注意的是,在这种方式下,AggregateRoot中的events字段是不能被持久化的,因为需要保证每次从数据库中加载出聚合根时events都是空的,为此在SaveEvents()保存了领域事件后,立即调用it.clearEvents()将所有的领域事件清空掉,以免领域事件随着聚合根一道被持久化到数据库中。

到目前为止,对领域事件的处理都还没有涉及到与任何消息中间件相关的内容,也即事件的产生是一个完全独立于消息队列的关注点,此时不用关心领域事件之后将以何种形式发布出去,Kafka 也好,RabbitMQ 也罢。除了关注点分离的好处外,这种解耦也使得系统在有可能切换消息中间件时更加的简单。

对于“在应用服务中通过eventPublisher.Publish()直接发布事件”而言,事件的产生和发布是同时完成的;但是对于“在聚合根中临时性保存领域事件”的方式来说,它只解决了事件的产生问题,并未解决事件的发布问题,事件的发布方应该采用“发射后不管(Fire And Forget)”的原则,即发布方无需了解消费方是如何处理领域事件的,甚至都不需要知道事件被哪些消费方所消费。

diff --git "a/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" "b/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" index 8c1575ec..e7adc5c0 100644 --- "a/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" +++ "b/2025/03/20/TDD-\345\222\214-DDD-\347\232\204\344\270\200\344\272\233\345\260\217\346\203\263\346\263\225/index.html" @@ -193,25 +193,31 @@

Test-Driven Development (TDD) 和 Domain-Driven Design (DDD) 是两种不同的软件开发方法论,各自有其独特的优缺点和应用场景。

-

Test-Driven Development (TDD)

优点:

    +

    Test-Driven Development (TDD)

    +

    优点:

    +
    1. 提高代码质量:通过编写测试来驱动开发,可以确保代码的正确性和可靠性。
    2. 减少缺陷:早期发现和修复缺陷,减少后期的调试和维护成本。
    3. 促进可维护性:代码更加模块化和可测试,便于后续的维护和扩展。
    4. 文档化:测试代码本身就是一种文档,描述了系统的行为和期望。
    5. 提高开发速度:虽然初期可能会慢一些,但长期来看,由于减少了重构和调试的时间,开发速度会提高。
    -

    缺点:

      +

      缺点:

      +
      1. 初期投入大:需要在开发前编写测试,初期投入的时间和精力较大。
      2. 学习曲线陡峭:对新手开发者来说,学习和掌握TDD需要一定的时间。
      3. 过度依赖测试:可能导致过度依赖单元测试,忽略了系统的整体性能和用户体验。
      -

      Domain-Driven Design (DDD)

      优点:

        +

        Domain-Driven Design (DDD)

        +

        优点:

        +
        1. 聚焦领域:通过深入理解业务领域,确保软件系统与业务需求紧密结合。
        2. 模型驱动:建立一个清晰的领域模型,帮助开发者和业务专家共同理解系统。
        3. 可维护性:通过明确的领域模型和边界,系统结构更加清晰,便于维护和扩展。
        4. 沟通桥梁:提供了一种通用的语言(Ubiquitous Language),促进开发团队和业务团队之间的沟通。
        -

        缺点:

          +

          缺点:

          +
          1. 复杂性高:DDD方法论较为复杂,需要深入理解领域模型和设计模式。
          2. 初期投入大:需要大量的时间和精力进行领域分析和建模。
          3. 适用范围有限:对于简单的系统或小型项目,DDD可能显得过于复杂和冗余。
          4. diff --git "a/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" "b/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" index eea6b710..5733b0ac 100644 --- "a/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" +++ "b/2025/03/20/\350\202\272\345\212\237\350\203\275\346\243\200\346\237\245\346\225\260\345\200\274/index.html" @@ -193,41 +193,51 @@

肺功能测试是评估患者呼吸系统健康的重要工具。各个指标的及其异常数值可能指示的潜在生理疾病:

-

1. FEV1(第一秒用力呼气量)

-

一、传统手艺:Partial Application

在函数式编程中,Partial Application 是传递依赖的常用方式。例如:

+

一、传统手艺:Partial Application

+

在函数式编程中,Partial Application 是传递依赖的常用方式。例如:

1
2
3
let foo bar baz request = ...
let wired = foo dependency1 dependency2
let response = wired request
- -

优点:

+

优点:

-

缺点:

+

缺点:


-

二、结构化方法:单一环境参数(env)

为解决参数爆炸问题,可将依赖封装为单一环境对象env,并通过接口约束访问权限:

+

二、结构化方法:单一环境参数(env)

+

为解决参数爆炸问题,可将依赖封装为单一环境对象env,并通过接口约束访问权限:

1
2
3
4
5
6
7
8
[<Interface>] type ILog = abstract Logger: ILogger
[<Interface>] type IDb = abstract Database: IDatabase

module Log =
let info (env: #ILog) = env.Logger.Info("Message")

module Db =
let fetchUser (env: #IDb) = env.Database.Query(...)
- -

优点:

+

优点:

-

应用场景:

+

应用场景:

1
2
3
4
5
let changePass env req = task {
let! user = Db.fetchUser env req.UserId
Log.info env "Processing user: %i" user.Id
...
}
-
-

三、Reader Monad

为消除显式的env传递,可引入 Reader Monad,将环境隐式注入计算流程:

+

三、Reader Monad

+

为消除显式的env传递,可引入 Reader Monad,将环境隐式注入计算流程:

1
2
3
4
5
6
7
8
9
10
11
[<Struct>] type Effect<'env, 'out> = Effect of ('env -> 'out)

module Effect =
let run env (Effect fn) = fn env
let bind f effect = Effect (fun env -> run env (f (run env effect)))

type EffectBuilder() =
member __.Bind(e, f) = Effect.bind f e
member __.Return(x) = Effect (fun _ -> x)

let effect = EffectBuilder()
-

然后:

1
2
3
4
5
6
let changePass req = effect {
let! user = Db.fetchUser req.UserId
let! salt = Random.bytes 32
do! Log.info "Password updated for user %i" user.Id
return Ok()
}
- -

优点:

+

优点:

-

缺点:

+

缺点:

-

Refs.

UUID v7 之所以能显著提升关系型数据库(尤其是采用聚集索引的 MySQL InnoDB)在插入和查询速度,主要归功于它在 ID 中引入了“可排序的时间戳前缀”,从而大幅减少了 B-Tree 索引页分裂(page split)和数据碎片化。这里分几点来说:

-

一、 聚集索引(Clustered Index)的插入机制

+

一、 聚集索引(Clustered Index)的插入机制

在 InnoDB 中,聚集索引的叶子节点同时存储了行数据,且按照索引键(主键)顺序物理排序。

-

当新记录的主键完全随机(如 UUID v4)时,每次插入都会随机落在 B-Tree 的不同叶子页,导致频繁的页分裂和指针重排,而页分裂和随机 I/O 会带来大量的磁盘写放大和缓存抖动(cache churn),削弱吞吐并拉高延迟。

-

二、UUID v7 的时间排序特性

-

UUID v7 在高位(前 48 位)嵌入了以毫秒级精度的 Unix 时间戳,剩下的位用于随机数或序列号,这样生成的 ID 保持全局唯一性的同时,随着时间自然递增(即“近似单调递增”),新插入的记录几乎总是追加到 B-Tree 的最右端叶子节点。

-

参见 dbaplus.cn 的分析:

+

当新记录的主键完全随机(如 UUID v4)时,每次插入都会随机落在 B-Tree 的不同叶子页,导致频繁的页分裂和指针重排,而页分裂和随机 I/O 会带来大量的磁盘写放大和缓存抖动(cache churn),削弱吞吐并拉高延迟。

+

二、UUID v7 的时间排序特性

+

UUID v7 在高位(前 48 位)嵌入了以毫秒级精度的 Unix 时间戳,剩下的位用于随机数或序列号,这样生成的 ID 保持全局唯一性的同时,随着时间自然递增(即“近似单调递增”),新插入的记录几乎总是追加到 B-Tree 的最右端叶子节点。

+

参见 dbaplus.cn 的分析:

“UUID v7 的创新之处在于其时间排序特性,它在前 48 位中嵌入了以毫秒为单位的 Unix 时间戳……可能在插入和查询操作上提供更好的性能”【1】。

-

三、降低页分裂与碎片化

+

三、降低页分裂与碎片化

顺序或近似顺序的主键能使叶子节点连续增长,极少触发页分裂,并且更少的页分裂意味着更低的写放大(write amplification)和更稳定的插入延迟,同时,减少了空洞和链表重排,提高了磁盘和内存缓存的命中率。

-

四、提升查询局部性与缓存命中

-

因为数据物理上是按时间顺序紧凑写入,时间范围查询(如 “最近 1 小时的日志”)可以快速定位连续的叶子页,I/O 更聚集,内存缓冲池(buffer pool)或操作系统页缓存能更有效地缓存最近热数据,进一步加速查询。

+

四、提升查询局部性与缓存命中

+

因为数据物理上是按时间顺序紧凑写入,时间范围查询(如 “最近 1 小时的日志”)可以快速定位连续的叶子页,I/O 更聚集,内存缓冲池(buffer pool)或操作系统页缓存能更有效地缓存最近热数据,进一步加速查询。


-

Rimon Tawadrous 在其 GitHub repo 中的测试,对比 100 万条逐条插入实验,UUID v7 相较 UUID v4 在单线程插入上速度快约 3.24%,多线程下更可观【1】。

+

Rimon Tawadrous 在其 GitHub repo 中的测试,对比 100 万条逐条插入实验,UUID v7 相较 UUID v4 在单线程插入上速度快约 3.24%,多线程下更可观【1】。


-

参考链接
[1] “为什么 UUID 7 比 UUID 4 更适合作为 RDBMS 的聚集索引?” dbaplus.cn
https://dbaplus.cn/news-160-6313-1.html
[2] “PostgreSQL and UUID as primary key” maciejwalkowiak
https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/
[3] “Optimised UUIDs in mysql” stitcher
https://stitcher.io/blog/optimised-uuids-in-mysql
[3] “Storing UUID Values in MySQL” percona
https://www.percona.com/blog/store-uuid-optimized-way/

+

参考链接
+[1] “为什么 UUID 7 比 UUID 4 更适合作为 RDBMS 的聚集索引?” dbaplus.cn
+https://dbaplus.cn/news-160-6313-1.html
+[2] “PostgreSQL and UUID as primary key” maciejwalkowiak
+https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/
+[3] “Optimised UUIDs in mysql” stitcher
+https://stitcher.io/blog/optimised-uuids-in-mysql
+[3] “Storing UUID Values in MySQL” percona
+https://www.percona.com/blog/store-uuid-optimized-way/

diff --git a/2025/04/20/linux-amd-screen-boom/index.html b/2025/04/20/linux-amd-screen-boom/index.html index 78a8c76f..41c36deb 100644 --- a/2025/04/20/linux-amd-screen-boom/index.html +++ b/2025/04/20/linux-amd-screen-boom/index.html @@ -195,7 +195,6 @@

不知道我们所说的闪烁是不是一个东西,在我的笔记本上,闪烁是指偶发的屏幕中出现部分彩色雪花。

我使用 Pop!_OS,此发行版使用 EFI 启动,故而可以在 /boot/efi/loader/loader.conf 中加上:

1
options quiet splash amdgpu.dcdebugmask=0x10 amdgpu.sg_display=0
-

对于其他使用 EFI 或 GRUB 启动的发行版也可以添加类似的参数尝试尝试。

diff --git a/2025/05/07/nestjs-bullmq-mail-business/index.html b/2025/05/07/nestjs-bullmq-mail-business/index.html index 6b36afbd..5eb3bcbd 100644 --- a/2025/05/07/nestjs-bullmq-mail-business/index.html +++ b/2025/05/07/nestjs-bullmq-mail-business/index.html @@ -192,48 +192,53 @@
-

在 BullMQ(以及它在 NestJS 里包装的 @Processor/WorkerHost)里,整个生命周期大致是这样的:

+

在 BullMQ(以及它在 NestJS 里包装的 @Processor/WorkerHost)里,整个生命周期大致是这样的:

    -
  1. 队列(在 NestJS 里由 @Processor 装饰的类)会被一个底层的 Worker 订阅。
  2. -
  3. 有新任务(job)进来时,Worker 会调用写在该类里的 async process(job: Job) 方法。
  4. -
  5. 如果 process() 正常返回(即没有抛异常),Job 就被标记为 completed,然后才会去触发所有注册了 @OnWorkerEvent('completed') 的回调。
  6. +
  7. 队列(在 NestJS 里由 @Processor 装饰的类)会被一个底层的 Worker 订阅。
  8. +
  9. 有新任务(job)进来时,Worker 会调用写在该类里的 async process(job: Job) 方法。
  10. +
  11. 如果 process() 正常返回(即没有抛异常),Job 就被标记为 completed,然后才会去触发所有注册了 @OnWorkerEvent('completed') 的回调。

也就是说:

而我在此处的业务目的是 “用队列来做可靠的、可重试的邮件发送”,那么一定要把发送邮件的逻辑写到 process() 里,这样在 commandBus.execute(new SendMailCommand(...)) 抛错时,BullMQ 会根据创建 JOB 时的重试策略(retry、backoff 等)自动重新入队。而把它放到 onCompleted(),只相当于 job 成功完成后的“事后通知”,一旦失败不会再重试,也无法利用 BullMQ 的锁、超时、重试机制。

-

举个最简化的调整示例,删掉 onCompleted,把真正的发信放到 process:

+

举个最简化的调整示例,删掉 onCompleted,把真正的发信放到 process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ... existing imports ...

@Processor(process.env.MAILER_QUEUE_NAME || "gcpm-mailer")
export class BullMQMailerProcesser extends WorkerHost {
constructor(
private readonly commandBus: CommandBus,
private readonly logger: LoggingService,
) {
super();
}

// ① 当有新 job 拉取到时,这个方法会被调用
public async process(job: Job): Promise<void> {
const mailAggregate = new Mail(job.data.mail);
try {
await this.commandBus.execute(new SendMailCommand(mailAggregate));
} catch (err) {
this.logger.error(`邮件发送失败,jobId=${job.id}`, err);
// 抛出错误,触发重试或失败
throw err;
}
}

// ② onCompleted 仅在 process() 正常返回后触发,
// 不建议在这里执行核心业务(也无法触发重试)。
// @OnWorkerEvent("completed")
// async onCompleted(job: Job) { … }
}
-

参考 BullMQ 官方文档:


而 重试次数本身并没有一个硬性上限,完全由添加 Job 时通过 attempts 这个选项来控制:

示例(给某封邮件最多重试 3 次):

1
2
3
4
5
6
7
8
9
10
11
await this.mailerQueue.add(
id,
{ mail: new Mail(/*…*/ ) },
{
attempts: 3, // 最多尝试 3 次
backoff: { // 重试时的延迟策略(可选)
type: 'exponential',
delay: 1000,
},
},
);
-

还有一个需要注意的地方,在我的业务中,邮件发送的是一种时间区间报告,这个报告包含了过去二十四小时的一些系统中的事件,但如果重试有延迟策略或重试本身就有计算成本的话,这封邮件就不是 “过去二十四小时” 的了,因为重试带来了一个真空期。

换言之,这个问题本质上是——重试导致「发送时刻」与「原始 24 小时窗口」错开,从而让邮件里报出来的数据不再精确。常见的解决思路就是:把「窗口定义」或者「报表内容」在调度时就固化下来,真正的队列任务只负责发送,而不再实时去重新计算时间区间。

我想到了两种解决方案:

-

一、任务参数里带上「时间区间」
在 enqueue 的时候,就算出 windowStart/windowEnd,然后把它放到 job.data 里。无论后面 process 什么时候真正跑,都是基于同一个时间区间去查询:

+

一、任务参数里带上「时间区间」
+在 enqueue 的时候,就算出 windowStart/windowEnd,然后把它放到 job.data 里。无论后面 process 什么时候真正跑,都是基于同一个时间区间去查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 调度时
const now = new Date();
const windowStart = new Date(now.getTime() - 24 * 60 * 60 * 1000);
await this.mailerQueue.add(
id,
{
mail: new Mail({
...options,
id,
sentAt: now,
status: MailStatus.PENDING,
windowStart,
windowEnd: now,
}),
},
{
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
},
);

// process 里
public async process(job: Job) {
const { windowStart, windowEnd } = job.data.mail;
// ① 只查询 [windowStart, windowEnd] 的事件
const events = await this.reportService.findEvents(windowStart, windowEnd);
const reportHtml = await this.reportService.renderReport(events);
await this.commandBus.execute(new SendMailCommand(job.data.mail, reportHtml));
}
-

➜ 这样无是马上执行还是几次重试后才执行,数据规则都不会变。

-

二、预先生成「静态报表内容」,挂到队列里
如果计算成本很高,或者怕重复查询数据开销大,也可以在调度时就把最终的 HTML/Text/附件 都先打好,然后作为 job.data 传进去,真正的 process() 只做一次“发送”即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 调度时:先生成报告
const now = new Date();
const windowStart = new Date(now.getTime() - 24*3600*1000);
const events = await this.reportService.findEvents(windowStart, now);
const reportHtml = await this.reportService.renderReport(events);

// 把静态内容塞到队列
await this.mailerQueue.add(
id,
{
mail: new Mail({ /*…*/, windowStart, windowEnd: now }),
reportHtml, // <- 预渲染好的文本/HTML
attachments: […], // <- 如果有附件也一并塞
},
{ attempts: 3, backoff: { type: 'fixed', delay: 5_000 } },
);

// process 里只关注发送
public async process(job: Job) {
try {
await this.mailService.send({
to: job.data.mail.to,
subject: `系统 24h 报表`,
html: job.data.reportHtml,
attachments: job.data.attachments,
});
} catch (e) {
throw e; // 触发重试
}
}

➜ 重试带来的任何延迟,都不影响邮件正文,始终是一份「事先约定好、并且静态化」的报告。

-

这两种模式都能保证最终发送时的数据窗口或内容,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算/少算”问题。

-

参考文档:

diff --git a/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html b/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html index 47a9feef..42f2895c 100644 --- a/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html +++ b/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html @@ -193,36 +193,41 @@

最近碰到一块业务:在系统中可以存在多个用户同时对某个项目信息进行编辑,这种多人协作的场景挺有意思的,不过在我们的业务中,并不需要实时协作,只需要保证不会出错就行,话虽如此,但也可以探索一下实时协作的实现方案,防止老年痴呆。

-

先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线/并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。

+

先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线/并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。

核心思想是:所有并发操作都是幂等(idempotent)、可交换(commutative)的。

-

常见类型有:
一、G-Counter(只能增计数器)
二、PN-Counter(可增可减计数器)
三、LWW-Register(最后写入胜出)
四、结合 JSON 的树型 CRDT(如 Automerge / Yjs)

-

更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”[^1]。

+

常见类型有:
+一、G-Counter(只能增计数器)
+二、PN-Counter(可增可减计数器)
+三、LWW-Register(最后写入胜出)
+四、结合 JSON 的树型 CRDT(如 Automerge / Yjs)

+

更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”[1]。

有一个挺有趣的 Rust 项目 Loro: Make your JSON data collaborative and version-controlled with CRDTs

-

假设我的项目信息编辑页面允许多人实时/离线修改某个研究项目的“名称”、“描述”字段,前端用 SvelteKit + GraphQL 获取和提交变更:

+

假设我的项目信息编辑页面允许多人实时/离线修改某个研究项目的“名称”、“描述”字段,前端用 SvelteKit + GraphQL 获取和提交变更:

1
2
┌── 用户 A 离线修改了 “description” 的若干段文本  
└── 用户 B 同时在线修改了同一字段的其他段落
-

如果后端使用 CRDT(比如把 description 用 JSON-CRDT 存储),两次修改只要在任意顺序合并都能得到完整的内容:

-

首先,A 客户端本地 apply 操作并缓存,恢复网络后推给服务器;
然后,服务器用 CRDT merge(A.delta, B.delta),得到一致文档
最后,服务器广播新文档到所有客户端,A/B 均得到相同结果

+

首先,A 客户端本地 apply 操作并缓存,恢复网络后推给服务器;
+然后,服务器用 CRDT merge(A.delta, B.delta),得到一致文档
+最后,服务器广播新文档到所有客户端,A/B 均得到相同结果


-

好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁/超时自动释放。

+

好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁/超时自动释放。

假如有一个这样的锁表:

-
CREATE TABLE project_lock (
+
CREATE TABLE project_lock (
     project_id UUID PRIMARY KEY,
     locked_by  UUID NOT NULL,
     expires_at TIMESTAMPTZ NOT NULL
 );
 

可以在事务内申请它:

-
const now = new Date();
+
const now = new Date();
 const expires = new Date(now.getTime() + 5*60*1000); // 5 分钟后过期
 await prisma.$transaction(async tx => {
     const existing = await tx.project_lock.findUnique({ where:{ project_id } });
         
     if (existing && existing.expires_at > now) {
-        throw new Error('项目正被人编辑');
-    }
+	    throw new Error('项目正被人编辑');
+	}
         
     await tx.project_lock.upsert({
         where: { project_id },
@@ -232,14 +237,16 @@ await prisma.$transaction(async tx => {
 });
 

释放锁就直接从锁表里删掉对应的数据即可:

-
await prisma.project_lock.delete({ where:{ project_id } });
+
await prisma.project_lock.delete({ where:{ project_id } });
 

前端的话,大概就是:

-

在进入编辑前请求一下 /api/project/:id/lock 之类的 API,失败则提示“被占用”;
在 onbeforeunload 时执行 /unlock;
超时后后端自动允许新锁。

+

在进入编辑前请求一下 /api/project/:id/lock 之类的 API,失败则提示“被占用”;
+在 onbeforeunload 时执行 /unlock;
+超时后后端自动允许新锁。


-

第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的版本号或时间戳;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。

+

第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的版本号或时间戳;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。

具体实现中,可以尝试在 project 表加上 version INT NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ ,然后更新项目时:

-
async updateProject(parent, { id, version, input }, ctx) {
+
async updateProject(parent, { id, version, input }, ctx) {
     const result = await prisma.$executeRaw`
     UPDATE project
         SET name        = ${input.name},
@@ -247,38 +254,51 @@ await prisma.$transaction(async tx => {
             version     = version + 1,
             updated_at  = now()
     WHERE id = ${id} AND version = ${version}
-    `;
+	`;
     if (result === 0) {
-        throw new ConflictException('版本冲突,请刷新后重试');
+	    throw new ConflictException('版本冲突,请刷新后重试');
     }
     return prisma.project.findUnique({ where:{ id } });
 }
 
-

前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并/重新加载?” 之类的玩意儿。

+

前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并/重新加载?” 之类的玩意儿。


-

第三个方案是:操作转化(Operational Transformation,OT)

-

也就是记录用户每次的“操作”(insert/delete at position),服务器根据历史操作序列对并发操作做转化(transform),确保先到达的操作调整后再应用后到达的。

-

有一些实现案例:
一、ShareDB(Node.js)
二、Google Docs 中的同步算法

-

具体实现的话,可能要现在前端逐字符/块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。

+

第三个方案是:操作转化(Operational Transformation,OT)

+

也就是记录用户每次的“操作”(insert/delete at position),服务器根据历史操作序列对并发操作做转化(transform),确保先到达的操作调整后再应用后到达的。

+

有一些实现案例:
+一、ShareDB(Node.js)
+二、Google Docs 中的同步算法

+

具体实现的话,可能要现在前端逐字符/块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。


最后可能还可以用事件溯源(Event Sourcing)+ 场景命令模式来实现:

-

不直接存状态,而是存所有“命令 / 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。

-

例如:
在每次更新时推送 ProjectUpdated { projectId, fieldsChanged, userId, timestamp } ,
然后写入事件存储(如 Kafka / EventStoreDB),
读端 Consumer 按顺序重建最新状态或按领域聚合 ,
最后在并发时如果两个事件都修改了同一字段,可在写端做校验/补偿,或在读端做最后写入胜出等策略 。

+

不直接存状态,而是存所有“命令 / 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。

+

例如:
+在每次更新时推送 ProjectUpdated { projectId, fieldsChanged, userId, timestamp } ,
+然后写入事件存储(如 Kafka / EventStoreDB),
+读端 Consumer 按顺序重建最新状态或按领域聚合 ,
+最后在并发时如果两个事件都修改了同一字段,可在写端做校验/补偿,或在读端做最后写入胜出等策略 。


总结来说,

    -
  • CRDT 最擅长 去中心化、离线编辑、自动合并;
  • -
  • 若不引入 CRDT,可根据业务侧重点选用:
      -
    1. 悲观锁 → 强制串行编辑,简单粗暴;
    2. -
    3. 乐观并发 → 适合大多数业务场景,成本低;
    4. -
    5. OT → 适合富文本或实时协同场景,复杂度中等;
    6. +
    7. CRDT 最擅长 去中心化、离线编辑、自动合并;
    8. +
    9. 若不引入 CRDT,可根据业务侧重点选用: +
        +
      1. 悲观锁 → 强制串行编辑,简单粗暴;
      2. +
      3. 乐观并发 → 适合大多数业务场景,成本低;
      4. +
      5. OT → 适合富文本或实时协同场景,复杂度中等;
      6. 事件溯源 → 适合需要全历史审计、可回放的场景。

-

[^1]: Decipad 博客 “Collaborative and Offline Editing Using CRDTs”
https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts

-

[^2]: Hacker News 讨论(CRDT 相关线程)
https://news.ycombinator.com/item?id=38289327

+
+
+
    +
  1. Decipad 博客 “Collaborative and Offline Editing Using CRDTs”
    +https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts ↩︎

    +
  2. +
+
diff --git a/2025/05/27/vertical-slicing-practice/index.html b/2025/05/27/vertical-slicing-practice/index.html index 98dee1bb..206a3e98 100644 --- a/2025/05/27/vertical-slicing-practice/index.html +++ b/2025/05/27/vertical-slicing-practice/index.html @@ -210,7 +210,7 @@

对于登录功能,需要考虑:

  • UI 层:登录表单(输入邮箱、密码的地方)、提交按钮、错误提示信息。
  • -
  • API/服务层:接收登录请求、验证用户凭证的接口。
  • +
  • API/服务层:接收登录请求、验证用户凭证的接口。
  • 业务逻辑层:校验输入格式、查询用户信息、验证密码、生成会话(Session)或令牌(Token)。
  • 数据访问层:从数据库中读取用户信息。
diff --git a/2025/06/30/ibs/index.html b/2025/06/30/ibs/index.html index fadd1e31..8818748d 100644 --- a/2025/06/30/ibs/index.html +++ b/2025/06/30/ibs/index.html @@ -193,29 +193,38 @@

肠易激综合征 (Irritable Bowel Syndrome, IBS) 是一种常见的功能性胃肠病。其核心特征是在没有发现任何器质性病变(如炎症、溃疡或肿瘤)的情况下,患者长期经历腹痛、腹胀,并伴有排便习惯的改变(腹泻、便秘或两者交替)。

-

一、 肠易激综合征的病理生理学

IBS 的病理生理机制非常复杂,并非由单一原因导致,而是多种因素相互作用的结果。目前的医学研究认为,其主要与以下几个核心机制有关:

-

1. 脑-肠轴功能紊乱 (Disorders of the Brain-Gut Axis)

这是 IBS 最核心的病理机制。大脑和肠道通过神经、内分泌和免疫系统形成一个双向沟通的“脑-肠轴”。在 IBS 患者中,这个轴的功能出现紊乱。

+

一、 肠易激综合征的病理生理学

+

IBS 的病理生理机制非常复杂,并非由单一原因导致,而是多种因素相互作用的结果。目前的医学研究认为,其主要与以下几个核心机制有关:

+

1. 脑-肠轴功能紊乱 (Disorders of the Brain-Gut Axis)

+

这是 IBS 最核心的病理机制。大脑和肠道通过神经、内分泌和免疫系统形成一个双向沟通的“脑-肠轴”。在 IBS 患者中,这个轴的功能出现紊乱。

  • 自上而下: 心理压力、焦虑、抑郁等负面情绪可以直接通过大脑影响肠道的运动、感觉和分泌功能,从而诱发或加重症状。
  • 自下而上: 肠道内的信号(如食物、气体、肠道菌群代谢产物)被过度放大后传给大脑,导致大脑对这些正常刺激产生过度反应,表现为疼痛和不适。
-

2. 内脏高敏感性 (Visceral Hypersensitivity)

这是指 IBS 患者的肠道对各种刺激(如食物、气体、肠壁的牵拉)异常敏感。正常人可以耐受的肠道内压力或活动,在 IBS 患者身上却会引发强烈的腹痛、急便感或不适。这种“放大”的感觉是 IBS 腹痛的主要原因。

-

3. 胃肠动力异常 (Altered Gut Motility)

肠道肌肉的收缩和舒张节律出现问题,导致食物和粪便在肠道内的转运速度异常。

+

2. 内脏高敏感性 (Visceral Hypersensitivity)

+

这是指 IBS 患者的肠道对各种刺激(如食物、气体、肠壁的牵拉)异常敏感。正常人可以耐受的肠道内压力或活动,在 IBS 患者身上却会引发强烈的腹痛、急便感或不适。这种“放大”的感觉是 IBS 腹痛的主要原因。

+

3. 胃肠动力异常 (Altered Gut Motility)

+

肠道肌肉的收缩和舒张节律出现问题,导致食物和粪便在肠道内的转运速度异常。

  • 转运过快: 肠道内容物过快通过结肠,水分来不及被充分吸收,导致腹泻(IBS-D)。
  • 转运过慢: 肠道蠕动乏力,粪便在结肠内停留时间过长,水分被过度吸收,导致便秘(IBS-C)。
  • 节律紊乱: 有时快有时慢,导致腹泻与便秘交替出现(IBS-M)。
-

4. 肠道微生态失调 (Alteration in Gut Microbiota)

IBS 患者的肠道菌群组成和功能与健康人存在差异。有害菌可能增多,有益菌(如双歧杆菌、乳酸杆菌)可能减少。这些失调的菌群会影响肠道屏障功能、产生过多气体、并激活免疫系统,导致腹胀和腹泻。

-

5. 肠道低度炎症与免疫激活 (Low-Grade Inflammation and Immune Activation)

部分 IBS 患者的肠道黏膜中可以观察到轻微的炎症反应,例如肥大细胞等免疫细胞数量增多并被激活。这些细胞会释放组胺、5-羟色胺等物质,直接刺激肠道神经,引起疼痛和动力异常。这种情况尤其在感染性肠炎后发生的 IBS(PI-IBS)中更为常见。

-

6. 遗传与饮食因素

    +

    4. 肠道微生态失调 (Alteration in Gut Microbiota)

    +

    IBS 患者的肠道菌群组成和功能与健康人存在差异。有害菌可能增多,有益菌(如双歧杆菌、乳酸杆菌)可能减少。这些失调的菌群会影响肠道屏障功能、产生过多气体、并激活免疫系统,导致腹胀和腹泻。

    +

    5. 肠道低度炎症与免疫激活 (Low-Grade Inflammation and Immune Activation)

    +

    部分 IBS 患者的肠道黏膜中可以观察到轻微的炎症反应,例如肥大细胞等免疫细胞数量增多并被激活。这些细胞会释放组胺、5-羟色胺等物质,直接刺激肠道神经,引起疼痛和动力异常。这种情况尤其在感染性肠炎后发生的 IBS(PI-IBS)中更为常见。

    +

    6. 遗传与饮食因素

    +
    • 遗传: IBS具有一定的家族聚集性,提示可能存在遗传易感性。
    • 饮食: 某些食物,特别是含有高FODMAPs(可发酵的寡糖、双糖、单糖和多元醇)的食物,在小肠内难以被吸收,进入大肠后被细菌快速发酵,产生大量气体并增加肠道渗透压,从而诱发腹胀、腹痛和腹泻。
    -

    二、 肠易激综合征的药物治疗

    +

    二、 肠易激综合征的药物治疗

    +

    IBS的治疗需要个体化,所有药物都应在医生指导下使用。医生应该根据具体的 IBS 亚型(腹泻型、便秘型、混合型)和主要症状来选择最合适的药物。

    -

    针对主要症状的药物

    1. 针对腹痛和腹胀(解痉药)

    +

    针对主要症状的药物

    +

    1. 针对腹痛和腹胀(解痉药)

    这类药物通过放松肠道平滑肌,缓解肠道痉挛来减轻疼痛。

    • 匹维溴铵 (Pinaverium Bromide): 作用于肠道平滑肌的钙离子通道,选择性高,副作用较小。
    • @@ -235,7 +244,8 @@
      • 容积性泻药(如欧车前、聚卡波非钙): 通过吸收水分增加粪便体积,刺激肠道蠕动。使用时需饮用足量水,否则可能加重便秘。
      • 渗透性泻药(如聚乙二醇 PEG、乳果糖): 通过在肠道内形成高渗环境,将水分保留在肠腔内,软化粪便,促进排便。相对安全,适合长期使用。
      • -
      • 促分泌药:
          +
        • 促分泌药: +
          • 鲁比前列酮 (Lubiprostone): 激活肠道氯离子通道,增加肠液分泌,软化粪便。
          • 利那洛肽 (Linaclotide)、普卡那肽 (Plecanatide): 激活鸟苷酸环化酶-C,促进肠液分泌,并能一定程度上缓解腹痛。
          diff --git a/2025/07/08/dadgad/index.html b/2025/07/08/dadgad/index.html index a1c061b4..044a3a33 100644 --- a/2025/07/08/dadgad/index.html +++ b/2025/07/08/dadgad/index.html @@ -194,9 +194,10 @@

          模态定弦的空弦音不直接构成传统意义上的大调或小调和弦,而是形成一种听感上更为空灵、悬浮的“挂留和弦”(Suspended Chord)效果。

          DADGAD (发音为 “dad-gad”): 这是凯尔特音乐(Celtic Music)中最具代表性的定弦。它的空弦音给人一种既非大调也非小调的模糊感,充满了神秘和悠远的色彩,非常适合营造氛围音乐和复杂的指弹旋律。

          -

          挂留和弦(Suspended Chord),通常简写为 “sus”,是一种在传统和弦结构上进行了巧妙“修改”的和弦。它的核心特点是:用一个非三度的音,去“替代”或“悬挂”了原本决定和弦大/小调色彩的三度音,从而创造出一种独特的声音效果。

          +

          挂留和弦(Suspended Chord),通常简写为 “sus”,是一种在传统和弦结构上进行了巧妙“修改”的和弦。它的核心特点是:用一个非三度的音,去“替代”或“悬挂”了原本决定和弦大/小调色彩的三度音,从而创造出一种独特的声音效果。

          想象一个标准的大三和弦或小三和弦,它由根音、三度音和五度音构成。其中,三度音是决定这个和弦听起来是明亮、开心的“大调”,还是忧郁、伤感的“小调”的关键。

          -

          而挂留和弦,就是把这个关键的“三度音”暂时拿掉,换上别的音。最常见的替代者有两个:
          一是 纯四度音 (Perfect 4th) ,二是 大二度音 (Major 2nd) 。

          +

          而挂留和弦,就是把这个关键的“三度音”暂时拿掉,换上别的音。最常见的替代者有两个:
          +一是 纯四度音 (Perfect 4th) ,二是 大二度音 (Major 2nd) 。

          sus4 和弦 (挂四和弦) 由 根音 (1) + 纯四度音 (4) + 纯五度音 (5) 构成,即用四度音替代了原本的三度音。听感上,这是最常见的挂留和弦。它创造出一种强烈而悬浮的张力感,声音既不属于大调也不属于小调,听起来非常“开阔”。例如,一个 Csus4 和弦,就是由 C、F、G 三个音构成,取代了标准C大调和弦(C-E-G)中的 E 音。

          sus2 和弦 (挂二和弦) 由 根音 (1) + 大二度音 (2) + 纯五度音 (5) 构成,特点是用二度音替代了原本的三度音。听感上,sus2和弦的声音比sus4更柔和、更明亮一些。它同样具有开放、模糊的色彩,但张力感稍弱,听起来更像是一种平静、梦幻的色彩和弦。例如,一个 Csus2 和弦,就是由 C、D、G 三个音构成,取代了标准C大调和弦(C-E-G)中的 E 音。

          而 DADGAD 定弦方式的空弦音,其核心就是一个挂留和弦,具体来说是一个 Dsus4 和弦。当你弹响 DADGAD 的所有空弦时,你实际上听到的就是 D、G、A 这三个音在不同八度上的叠加和共鸣。这三个音不多不少,正好构成了 Dsus4 和弦。

          diff --git a/2025/07/11/fuck-hcho/index.html b/2025/07/11/fuck-hcho/index.html index a3d153a1..6692c0f5 100644 --- a/2025/07/11/fuck-hcho/index.html +++ b/2025/07/11/fuck-hcho/index.html @@ -192,11 +192,12 @@
-

前几天和群友聊到甲醛相关的东东,有点好奇甲醛进入人体之后的一系列反应,于逝,我去吸了两口 ,先来看看一些无聊的总结:

+

前几天和群友聊到甲醛相关的东东,有点好奇甲醛进入人体之后的一系列反应,于逝,我去吸了两口 ,先来看看一些无聊的总结:

吸入甲醛后,人体之所以会产生眼部灼痛、喉咙刺痒、呼吸困难等一系列强烈的不适感,是因为甲醛作为一种高活性、强刺激性的化学物质,对我们的身体构成了从宏观到微观的多层次、多途径的直接攻击。其影响方式主要可以归结为两大方面:直接的刺激与腐蚀作用和深层的细胞毒性与遗传毒性。

当甲醛气体通过呼吸进入人体,首当其冲的是我们的眼睛和呼吸道黏膜。这些部位的细胞直接暴露在甲醛的攻击下,引发一系列即时且强烈的不适症状:

    -
  • 对黏膜的强烈刺激: 甲醛具有极强的亲水性和反应活性,能迅速与眼睛结膜、鼻腔、咽喉和气管等部位黏膜表面的蛋白质和水分发生反应。这种反应会直接损伤黏膜组织,导致神经末梢受到强烈刺激。
      +
    • 对黏膜的强烈刺激: 甲醛具有极强的亲水性和反应活性,能迅速与眼睛结膜、鼻腔、咽喉和气管等部位黏膜表面的蛋白质和水分发生反应。这种反应会直接损伤黏膜组织,导致神经末梢受到强烈刺激。 +
      • 眼部症状: 眼睛会感到灼烧、刺痛、发痒和不自主流泪。
      • 呼吸道症状: 喉咙会感到干燥、刺痛、咳嗽,高浓度下更会引发胸闷、气喘,甚至喉头水肿和肺水肿,导致呼吸极度困难。
      @@ -207,66 +208,82 @@
      • 蛋白质变性: 甲醛能够与构成生命基础的蛋白质中的氨基(-NH2)发生反应,形成“亚甲基桥”,破坏蛋白质原有的三维结构,使其失去正常的生理功能。细胞的功能依赖于各种蛋白质(酶、结构蛋白等)的正常工作,蛋白质变性意味着细胞代谢紊乱、功能障碍,甚至死亡。这在本质上是一种“腐蚀”作用,也是福尔马林(甲醛水溶液)能用作防腐剂的根本原因。
      • 诱导细胞凋亡与氧化应激: 研究表明,甲醛会损伤细胞内的能量工厂——线粒体,诱导细胞产生大量的活性氧(ROS)。过量的活性氧会破坏细胞膜、蛋白质和DNA,引发“氧化应激”反应,并最终启动细胞的程序性死亡(凋亡)机制。
      • -
      • 遗传毒性与致癌性: 甲醛最具危害性的影响在于其遗传毒性。它可以穿过细胞核,与遗传物质DNA和组蛋白(包裹DNA的蛋白质)形成“加合物”,这会干扰DNA的正常复制和修复过程,导致基因突变。
          +
        • 遗传毒性与致癌性: 甲醛最具危害性的影响在于其遗传毒性。它可以穿过细胞核,与遗传物质DNA和组蛋白(包裹DNA的蛋白质)形成“加合物”,这会干扰DNA的正常复制和修复过程,导致基因突变。 +
          • 致癌风险: 当这种基因损伤发生在关键的癌基因或抑癌基因上,并且人体的修复机制无法有效清除时,就可能导致细胞的异常增殖,最终引发癌症。世界卫生组织(WHO)下属的国际癌症研究机构(IARC)已将甲醛列为一类致癌物,有充分证据表明其与鼻咽癌、白血病等恶性肿瘤的发生密切相关。
        • 神经与免疫毒性: 甲醛还能对中枢神经系统产生毒性作用,导致头晕、头痛、记忆力减退、失眠等症状。同时,它也可能干扰免疫系统的正常功能,使人更容易过敏(如引发过敏性皮炎、哮喘)或抵抗力下降。

        -

        甲醛与粘膜表面的蛋白质和水分发生反应?

        当甲醛(HCHO)这个小而活泼的分子,遇到我们湿润的眼睛、鼻腔和喉咙黏膜时,它首先会迅速溶于黏膜表面的水分,形成甲二醇 (CH₂(OH)₂) 。这个形态的甲醛会将目标转向 蛋白质。

        +

        甲醛与粘膜表面的蛋白质和水分发生反应?

        +

        当甲醛(HCHO)这个小而活泼的分子,遇到我们湿润的眼睛、鼻腔和喉咙黏膜时,它首先会迅速溶于黏膜表面的水分,形成甲二醇 (CH₂(OH)₂) 。这个形态的甲醛会将目标转向 蛋白质。

        这个反应并非简单的接触,而是一种被称为“交联反应”(Cross-linking)的化学过程。具体来说:甲醛分子会精准地找到蛋白质分子链上的氨基(-NH₂),这是许多氨基酸的共同特征。接着与蛋白质的氨基发生反应,形成一个不稳定的中间体。这个中间体可以进一步与邻近的另一个蛋白质的氨基反应,从而在两个原本独立的蛋白质(或同一个蛋白质的不同部分)之间,强行架起一座“亚甲基桥”(-CH₂-)。

        蛋白质就像一团经过精确折叠、有着特定三维结构的精密机器,它的功能完全依赖于这个构型。而甲醛的“交联”反应,就像是往这台精密机器的各个活动关节之间,甚至和旁边的机器之间倒 502,给它们粘死了。

        -

        为什么甲醛分子能精准地找到蛋白质分子链上的氨基?

        “精准”这两个字,听起来像是一个有思想、有目标的动作,但实际上是一场由分子结构和电荷分布主导的化学反应。

        -

        甲醛分子(HCHO)的核心是一个羰基(C=O)。在这个结构中,氧原子(O)是一个电负性非常强的元素,它对电子有着极强的吸引力。它会大力地将碳氧双键中的共用电子云拉向自己。这种电子云的偏移,导致了与它相连的碳原子(C)周围的电子变得非常稀薄,从而带上了部分正电荷(δ+)。

        +

        为什么甲醛分子能精准地找到蛋白质分子链上的氨基?

        +

        “精准”这两个字,听起来像是一个有思想、有目标的动作,但实际上是一场由分子结构和电荷分布主导的化学反应。

        +

        甲醛分子(HCHO)的核心是一个羰基(C=O)。在这个结构中,氧原子(O)是一个电负性非常强的元素,它对电子有着极强的吸引力。它会大力地将碳氧双键中的共用电子云拉向自己。这种电子云的偏移,导致了与它相连的碳原子(C)周围的电子变得非常稀薄,从而带上了部分正电荷(δ+)。

        在化学上,这样一个“缺少电子”、对电子充满渴望的原子或基团,被称为“亲电体”(Electrophile)。

        氨基(-NH₂)在蛋白质中广泛存在,尤其是在赖氨酸、精氨酸等氨基酸的侧链上。氨基的核心是氮原子(N)。根据它的电子排布,氮原子的最外层有一对没有参与形成化学键的电子,这叫做 “孤对电子”(Lone Pair Electrons)。 这对孤对电子,使得氮原子成了一个电子云密度很高的区域,带上了部分负电荷(δ-)。

        这样一个拥有“富余”电子、并愿意用它去攻击其他正电中心的原子或基团,在化学上被称为 “亲核体”(Nucleophile)。

        当甲醛分子靠近蛋白质时,甲醛分子上带部分正电荷的碳原子(亲电体),与蛋白质氨基上带部分负电荷的氮原子(亲核体),会因基本的静电引力而相互靠近。一旦距离足够近,氨基上慷慨的“孤对电子”会毫不犹豫地“攻击”甲醛上那个渴望电子的碳原子,并与它形成一个新的化学键。

        这个过程,就是化学中的“亲核加成反应”。

        -

        这个反应为何会损伤黏膜组织?

        蛋白质被甲醛“交联” 这个过程在化学上称为 “蛋白质变性”,它直接导致了组织损伤。

        +

        这个反应为何会损伤黏膜组织?

        +

        蛋白质被甲醛“交联” 这个过程在化学上称为 “蛋白质变性”,它直接导致了组织损伤。

        首先黏膜细胞需要各种酶(本身也是蛋白质)来维持新陈代谢。被交联后,这些酶的结构被破坏,失去了活性,细胞的正常生理活动瞬间停滞。而细胞的骨架、细胞膜上的离子通道等结构蛋白,同样会被交联破坏。这导致细胞失去原有的形态,细胞膜的通透性被改变,甚至直接破裂。当大量的细胞功能瘫痪、结构崩解后,它们会迅速走向死亡。成片的细胞死亡,就意味着我们宏观可见的组织损伤。这本质上是一种微观的化学性灼伤。

        这也是为什么福尔马林(甲醛水溶液)能用来制作标本——它通过“交联”反应,将生物组织的所有蛋白质“固化”,使其永不腐败,但这对于活体组织而言,就是彻头彻尾的毁灭。

        -

        为何神经末梢会受到强烈刺激?

        我们的黏膜组织下,密布着丰富的神经末梢。

        +

        为何神经末梢会受到强烈刺激?

        +

        我们的黏膜组织下,密布着丰富的神经末梢。

        完好的黏膜上皮细胞是神经末梢的保护层。当这层保护被甲醛破坏,下方的神经末梢就直接暴露了出来。此时,高活性的甲醛分子可以直接攻击这些裸露的神经末梢,就像用化学品直接触碰神经一样,瞬间产生剧烈的刺激信号。

        被损伤和死亡的细胞,在“弥留之际”会释放出大量的化学信号物质,如前列腺素、缓激肽、组胺等。这些物质被称为“炎症介质”。而这些炎症介质本身,就是神经末梢上痛觉感受器的强效激活剂。它们会放大并持续地刺激神经,导致灼痛、瘙痒和刺痛感。

        -

        什么是活性氧?

        活性氧(Reactive Oxygen Species, ROS) 是一类含氧的、化学性质极其活泼的分子或自由基的总称。常见的活性氧有 超氧阴离子(O₂⁻•)、羟自由基(•OH)、过氧化氢(H₂O₂)等。它们的特点之一是其都带有“未配对的电子”,这使得它们在化学上极不稳定,迫切地想要从其他分子那里“抢夺”电子来让自己稳定下来。

        -

        在正常情况下,我们身体会产生少量活性氧。它们是重要的信号分子,参与调节细胞生长、分化,并且是免疫细胞(如巨噬细胞)用来消灭入侵病原体(细菌、病毒)的“化学武器”。

        -

        一旦活性氧的产生远远超过了细胞自身的清除能力(细胞内有超氧化物歧化酶SOD等“警察”来控制它们),就会形成“氧化应激”(Oxidative Stress)状态。这些失控的“小流氓”就会在细胞内肆意破坏,攻击一切它们遇到的分子。

        -

        甲醛如何诱导线粒体产生大量活性氧?

        线粒体是通过一个叫做“电子传递链”的复杂过程来产生能量(ATP)的。就像一条生产线,电子(e⁻)在这条生产线上被一步步传递,最终与我们吸入的氧气(O₂)结合生成水(H₂O),同时释放出大量能量。

        +

        什么是活性氧?

        +

        活性氧(Reactive Oxygen Species, ROS) 是一类含氧的、化学性质极其活泼的分子或自由基的总称。常见的活性氧有 超氧阴离子(O₂⁻•)、羟自由基(•OH)、过氧化氢(H₂O₂)等。它们的特点之一是其都带有“未配对的电子”,这使得它们在化学上极不稳定,迫切地想要从其他分子那里“抢夺”电子来让自己稳定下来。

        +

        在正常情况下,我们身体会产生少量活性氧。它们是重要的信号分子,参与调节细胞生长、分化,并且是免疫细胞(如巨噬细胞)用来消灭入侵病原体(细菌、病毒)的“化学武器”。

        +

        一旦活性氧的产生远远超过了细胞自身的清除能力(细胞内有超氧化物歧化酶SOD等“警察”来控制它们),就会形成“氧化应激”(Oxidative Stress)状态。这些失控的“小流氓”就会在细胞内肆意破坏,攻击一切它们遇到的分子。

        +

        甲醛如何诱导线粒体产生大量活性氧?

        +

        线粒体是通过一个叫做“电子传递链”的复杂过程来产生能量(ATP)的。就像一条生产线,电子(e⁻)在这条生产线上被一步步传递,最终与我们吸入的氧气(O₂)结合生成水(H₂O),同时释放出大量能量。

        而甲醛及其代谢产物(如甲酸)会直接损伤电子传递链上的复合体蛋白(特别是复合体I和III)。这就像是破坏了生产线上的关键机器。

        当生产线上的机器受损或堵塞时,本该被有序传递的电子就会从传递链上“泄漏”出来。

        这些泄漏出来的高能电子,会直接传递给旁边无辜的氧气分子(O₂)。正常的氧气分子得到一个额外的电子后,就成为了极不稳定的超氧阴离子(O₂⁻•) (活性氧)。

        -

        为什么活性氧能破坏细胞膜、蛋白质和DNA?

        失控的活性氧会疯狂地从周围的生物大分子上抢夺电子,这个过程会导致这些大分子的化学结构被破坏,失去原有功能。

        +

        为什么活性氧能破坏细胞膜、蛋白质和DNA?

        +

        失控的活性氧会疯狂地从周围的生物大分子上抢夺电子,这个过程会导致这些大分子的化学结构被破坏,失去原有功能。

        细胞膜主要由磷脂双分子层构成,其中含有大量的不饱和脂肪酸。活性氧(特别是羟自由基•OH)会攻击不饱和脂肪酸中的双键,抢走一个电子,引发一个叫做“脂质过氧化”的链式反应。脂质过氧化会破坏细胞膜的流动性和完整性,使其变得僵硬、脆裂,最终导致细胞膜破裂,细胞内容物泄漏。

        -

        而活性氧会攻击蛋白质侧链上的氨基酸(特别是半胱氨酸、甲硫氨酸等),使其氧化、交联(和甲醛的作用类似但原理不同),导致蛋白质变性,失去作为酶、结构蛋白或信号分子的功能。

        -

        活性氧还会攻击DNA碱基(特别是鸟嘌呤G)和脱氧核糖骨架,导致碱基修饰、DNA单链或双链断裂。这会直接导致基因突变,干扰DNA的复制和转录,是诱发癌症的重要原因。

        -

        什么是细胞的程序性死亡?

        细胞程序性死亡(Programmed Cell Death),最主要的形式是细胞凋亡(Apoptosis)。这并非细胞因损伤而发生的被动解体(那是“坏死”),而是细胞在接收到特定信号后,主动启动的一套基因编码的“自杀程序”。目的是清除体内不再需要、衰老或受到严重损伤(如无法修复的DNA损伤)的细胞,从而维持组织器官的稳定和健康。这是一个对机体有利的主动行为。

        +

        而活性氧会攻击蛋白质侧链上的氨基酸(特别是半胱氨酸、甲硫氨酸等),使其氧化、交联(和甲醛的作用类似但原理不同),导致蛋白质变性,失去作为酶、结构蛋白或信号分子的功能。

        +

        活性氧还会攻击DNA碱基(特别是鸟嘌呤G)和脱氧核糖骨架,导致碱基修饰、DNA单链或双链断裂。这会直接导致基因突变,干扰DNA的复制和转录,是诱发癌症的重要原因。

        +

        什么是细胞的程序性死亡?

        +

        细胞程序性死亡(Programmed Cell Death),最主要的形式是细胞凋亡(Apoptosis)。这并非细胞因损伤而发生的被动解体(那是“坏死”),而是细胞在接收到特定信号后,主动启动的一套基因编码的“自杀程序”。目的是清除体内不再需要、衰老或受到严重损伤(如无法修复的DNA损伤)的细胞,从而维持组织器官的稳定和健康。这是一个对机体有利的主动行为。

        在这个过程中每,细胞会主动收缩、染色质固缩、细胞核碎裂,最终形成多个被完整细胞膜包裹的“凋亡小体”,等待被免疫细胞吞噬清理。整个过程非常“干净”,不会引发周围的炎症。

        -

        在甲醛诱导的氧化应激中,由活性氧造成的大量、无法修复的DNA损伤和蛋白质损伤,本身就是最强烈的“死亡信号”。这个信号会激活细胞内一类叫做Bax和Bak的“促凋亡蛋白”。被激活的Bax/Bak蛋白会聚集到线粒体外膜上,像一个“打孔器”一样,在线粒体外膜上形成孔道。线粒体外膜被打孔后,原本储存在线粒体内部的一种关键蛋白——细胞色素C(Cytochrome C),就会被释放到细胞质中。释放到细胞质中的细胞色素C,会激活一系列被称为 Caspase 的蛋白酶。它们一旦被激活,就会形成一个级联反应,像多米诺骨牌一样,一个激活下一个。被激活的Caspase会系统性地降解细胞内的关键蛋白质和DNA,最终导致细胞有序地解体,完成凋亡。

        +

        在甲醛诱导的氧化应激中,由活性氧造成的大量、无法修复的DNA损伤和蛋白质损伤,本身就是最强烈的“死亡信号”。这个信号会激活细胞内一类叫做Bax和Bak的“促凋亡蛋白”。被激活的Bax/Bak蛋白会聚集到线粒体外膜上,像一个“打孔器”一样,在线粒体外膜上形成孔道。线粒体外膜被打孔后,原本储存在线粒体内部的一种关键蛋白——细胞色素C(Cytochrome C),就会被释放到细胞质中。释放到细胞质中的细胞色素C,会激活一系列被称为 Caspase 的蛋白酶。它们一旦被激活,就会形成一个级联反应,像多米诺骨牌一样,一个激活下一个。被激活的Caspase会系统性地降解细胞内的关键蛋白质和DNA,最终导致细胞有序地解体,完成凋亡。

        “激活”在这里指的是一个剧烈的“构象变化”(Conformational Change),即蛋白质从一种三维折叠形态,转变为另一种形态。

        -

        什么是Bax/Bak蛋白?

        Bax和Bak是细胞内一个庞大家族——Bcl-2蛋白家族中的两个关键成员。Bax和Bak是在这里具有代表性的促凋亡派(Pro-apoptotic),Bcl-2、Bcl-xL 是 抗凋亡派(Anti-apoptotic):

        +

        什么是Bax/Bak蛋白?

        +

        Bax和Bak是细胞内一个庞大家族——Bcl-2蛋白家族中的两个关键成员。Bax和Bak是在这里具有代表性的促凋亡派(Pro-apoptotic),Bcl-2、Bcl-xL 是 抗凋亡派(Anti-apoptotic):

        在正常健康的细胞中,它们处于一种无害的、折叠起来的休眠状态。Bax大部分时间游荡在细胞质中。Bak 则停靠在线粒体的外膜上,但同样处于被抑制的休眠状态。

        当细胞遭受了大量、无法修复的DNA或蛋白质损伤时,这个信号会被细胞内的 p53蛋白 感知到。被激活的p53蛋白,会生产大量的一类被称为 “仅含BH3结构域蛋白”(BH3-only proteins)的小蛋白,例如 PUMA 和 Noxa。

        -

        Bax/Bak 蛋白如何打孔?

        当Bax被激活后,它会暴露内部的疏水结构域。这个结构域极度“讨厌”水(细胞质的主要成分是水),而非常“喜欢”油性的环境。线粒体外膜正是一个由磷脂双分子层构成的“油性”环境。 因此,被激活的Bax会像一滴油会自动融入另一滴油一样,自发地从细胞质中转移并插入到线粒体外膜上。而已在膜上的Bak,被激活后则会更深、更稳定地嵌入膜中。

        -

        这是一个寡聚化(Oligomerization)过程。多个被激活的Bax/Bak分子在膜上相遇,会像乐高积木一样,通过彼此暴露出来的结构域互相识别并拼接在一起,形成大小不一的聚合体(Oligomer),有的像线形,有的像弧形,有的像不完整的环。这些Bax/Bak聚合体并不是贯穿膜形成一个隧道。相反,它们像楔子一样,大量地、浅浅地插入到线粒体外膜的外层。当越来越多的蛋白质“楔子”挤进这一层薄薄的膜时,会产生巨大的物理张力和弯曲应力。

        -

        最终,线粒体外膜因为无法承受这种内部应力,就像一个被过度拉伸的气球一样被撕裂,形成的“孔道”或“裂口”,其边缘是由蛋白质和脂质分子共同构成的,被称为 “蛋白-脂质孔道”(Proteolipidic Pore)。

        -

        为什么“严重损伤”会激活p53蛋白?

        在正常、安逸的细胞中,p53蛋白一被生产出来,就会被一个叫做 MDM2 的蛋白“盯上”。MDM2会给p53贴上一个叫做 “泛素”(Ubiquitin)的标签,被贴上标签的p53会被送到细胞的“垃圾处理厂”——蛋白酶体中迅速降解。因此,正常细胞里的p53蛋白水平极低。

        +

        Bax/Bak 蛋白如何打孔?

        +

        当Bax被激活后,它会暴露内部的疏水结构域。这个结构域极度“讨厌”水(细胞质的主要成分是水),而非常“喜欢”油性的环境。线粒体外膜正是一个由磷脂双分子层构成的“油性”环境。 因此,被激活的Bax会像一滴油会自动融入另一滴油一样,自发地从细胞质中转移并插入到线粒体外膜上。而已在膜上的Bak,被激活后则会更深、更稳定地嵌入膜中。

        +

        这是一个寡聚化(Oligomerization)过程。多个被激活的Bax/Bak分子在膜上相遇,会像乐高积木一样,通过彼此暴露出来的结构域互相识别并拼接在一起,形成大小不一的聚合体(Oligomer),有的像线形,有的像弧形,有的像不完整的环。这些Bax/Bak聚合体并不是贯穿膜形成一个隧道。相反,它们像楔子一样,大量地、浅浅地插入到线粒体外膜的外层。当越来越多的蛋白质“楔子”挤进这一层薄薄的膜时,会产生巨大的物理张力和弯曲应力。

        +

        最终,线粒体外膜因为无法承受这种内部应力,就像一个被过度拉伸的气球一样被撕裂,形成的“孔道”或“裂口”,其边缘是由蛋白质和脂质分子共同构成的,被称为 “蛋白-脂质孔道”(Proteolipidic Pore)。

        +

        为什么“严重损伤”会激活p53蛋白?

        +

        在正常、安逸的细胞中,p53蛋白一被生产出来,就会被一个叫做 MDM2 的蛋白“盯上”。MDM2会给p53贴上一个叫做 “泛素”(Ubiquitin)的标签,被贴上标签的p53会被送到细胞的“垃圾处理厂”——蛋白酶体中迅速降解。因此,正常细胞里的p53蛋白水平极低。

        当 “严重损伤”,尤其是DNA双链断裂(这被视为细胞最危急的损伤之一)发生时,DNA断裂处会立刻吸引并激活一批蛋白,其中最关键的是一类激酶(Kinase),比如ATM和ATR。激酶是一种能给其他蛋白质“安装”上磷酸基团(-PO₃²⁻)的工具酶。

        -

        被激活的ATM/ATR激酶,会立刻找到 MDM2,并给它装上好几个磷酸基团。被磷酸化后的MDM2结构发生改变,无法再识别和结合p53。 与此同时,ATM/ATR 等激酶也会给 p53 蛋白本身装上磷酸基团。这个过程不仅让p53无法被MDM2识别,更重要的是,它改变了p53的构象。

        -

        对于p53来说,最基础的激活,就是摆脱 MDM2 的控制,不再被降解。这使得 p53 蛋白可以在细胞核内大量、稳定地积累起来,被磷酸化等一系列化学修饰后,p53蛋白的三维结构会发生精妙的变化。这种变化会暴露p53内部一个关键的功能区——“DNA结合域”。同时,它会促使四个p53蛋白分子结合在一起,形成一个四聚体(Tetramer)。

        -
        为什么 MDM2 会盯上 p53?

        在MDM2蛋白的N端(头部),有一个精心构造的、深邃的疏水性口袋(hydrophobic pocket),就像手套一样。而在p53蛋白的N端(反式激活域),有一段α-螺旋区域。这段螺旋上有三个关键的氨基酸残基——苯丙氨酸(Phe19)、色氨酸(Trp23)和亮氨酸(Leu26)。这三个氨基酸像三根关键的“手指”,完美地伸入并契合MDM2的那个手套中。

        -
        p53被激活后如何“下令”生产PUMA等蛋白?

        PUMA这个蛋白,它的“设计图纸”编码在一段叫做 _PUMA_(也称 _BBC3_)的基因里。在这段基因的上游,有一段特殊的DNA序列,叫做 “启动子”(Promoter)。而PUMA基因的启动子上,有 p53 蛋白能够识别并结合的特定“停泊位点”,称为 “p53响应元件”(p53 Response Element)。

        +

        被激活的ATM/ATR激酶,会立刻找到 MDM2,并给它装上好几个磷酸基团。被磷酸化后的MDM2结构发生改变,无法再识别和结合p53。 与此同时,ATM/ATR 等激酶也会给 p53 蛋白本身装上磷酸基团。这个过程不仅让p53无法被MDM2识别,更重要的是,它改变了p53的构象。

        +

        对于p53来说,最基础的激活,就是摆脱 MDM2 的控制,不再被降解。这使得 p53 蛋白可以在细胞核内大量、稳定地积累起来,被磷酸化等一系列化学修饰后,p53蛋白的三维结构会发生精妙的变化。这种变化会暴露p53内部一个关键的功能区——“DNA结合域”。同时,它会促使四个p53蛋白分子结合在一起,形成一个四聚体(Tetramer)。

        +
        为什么 MDM2 会盯上 p53?
        +

        在MDM2蛋白的N端(头部),有一个精心构造的、深邃的疏水性口袋(hydrophobic pocket),就像手套一样。而在p53蛋白的N端(反式激活域),有一段α-螺旋区域。这段螺旋上有三个关键的氨基酸残基——苯丙氨酸(Phe19)、色氨酸(Trp23)和亮氨酸(Leu26)。这三个氨基酸像三根关键的“手指”,完美地伸入并契合MDM2的那个手套中。

        +
        p53被激活后如何“下令”生产PUMA等蛋白?
        +

        PUMA这个蛋白,它的“设计图纸”编码在一段叫做 PUMA(也称 BBC3)的基因里。在这段基因的上游,有一段特殊的DNA序列,叫做 “启动子”(Promoter)。而PUMA基因的启动子上,有 p53 蛋白能够识别并结合的特定“停泊位点”,称为 “p53响应元件”(p53 Response Element)。

        被激活的p53四聚体,凭借其暴露出来的DNA结合域,会精准地找到并牢牢地结合在PUMA基因的这个“停泊位点”上。一旦结合,p53 自身就会像一块磁铁,吸引来细胞核内负责“抄写”基因的庞大机器——RNA聚合酶II以及其他大量的辅助蛋白。这个庞大的蛋白质复合体,就是负责执行命令的“抄写员”。

        “抄写员”以DNA为模板,“抄写”出一段信使RNA(mRNA)。这段mRNA会离开细胞核进入细胞质,并被核糖体(蛋白质的“生产工厂”)读取,最终翻译、生产出我们需要的PUMA蛋白。

        这就是基因转录。

        -

        为什么 DNA 的断裂会吸引激酶?

        在正常细胞中,ATM是以无活性的二聚体(两个ATM蛋白抱在一起)形式存在的。这个“拥抱”的姿态,恰好就掩盖了彼此的“催化核心区”。 激活,就是让这俩分开。

        +

        为什么 DNA 的断裂会吸引激酶?

        +

        在正常细胞中,ATM是以无活性的二聚体(两个ATM蛋白抱在一起)形式存在的。这个“拥抱”的姿态,恰好就掩盖了彼此的“催化核心区”。 激活,就是让这俩分开。

        当DNA发生双链断裂时,会暴露出两个DNA断端。这在正常的、连续的DNA双螺旋结构中是绝对不会出现的。这个裸露的、不连续的断端,就是一个强烈的物理异常信号。

        细胞内有一组蛋白质,叫做MRN复合体(由Mre11、Rad50、Nbs1三个蛋白组成)。

        Mre11和Rad50 能特异性地识别并抓住DNA的断端,像桥梁一样将两个断端物理性地拉近,防止它们飘远。MRN复合体一旦抓住了DNA断端,它自身的构象就会发生改变。这种改变使得它能够吸引在细胞核内游走的、处于休眠二聚体状态的ATM。MRN复合体(特别是其中的Nbs1蛋白)上会暴露出一个能与ATM二聚体结合的“接口”。这种分子间的特异性亲和力,使得ATM被“吸引”并物理性地富集到DNA断裂的地方。

        当大量的ATM二聚体被招募到MRN复合体标记的DNA断端附近时,DNA断端的物理存在,加上MRN复合体的“撬动”作用,会诱导ATM二聚体的构象发生变化,促使这俩解离,变成两个独立的单体。

        -

        解离后的ATM单体,其被隐藏的“催化核心区”立刻暴露了出来。一个ATM单体会立刻给另一个(或自己)的特定位点(如丝氨酸1981位点)“安装”上一个磷酸基团。这是个 “自我磷酸化” 的过程,会使其构象进一步稳定在完全激活的状态。

        -

        先到这儿吧,我要吃炒粉了

        +

        解离后的ATM单体,其被隐藏的“催化核心区”立刻暴露了出来。一个ATM单体会立刻给另一个(或自己)的特定位点(如丝氨酸1981位点)“安装”上一个磷酸基团。这是个 “自我磷酸化” 的过程,会使其构象进一步稳定在完全激活的状态。

        +

        先到这儿吧,我要吃炒粉了

        +