tikv事务(事务嵌套 索引和数据本身)

以下是改进后的 **SQLAlchemy 嵌套事务(基于保存点)** 的完整代码,明确区分内外层事务的提交与回滚逻辑:

---

### **改进点说明**
1. **使用 `nested_trans.commit()` 替代 `session.commit()`**,明确表示内层事务的提交仅释放保存点。
2. **严格分离内外层事务控制**,避免直接调用 `session.commit()` 导致逻辑混淆。
3. **完善异常处理**,确保所有操作均在事务边界内。

---

### **完整代码示例**
```python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 定义数据模型(示例)
Base = declarative_base()

class Account(Base):
    __tablename__ = 'accounts'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer)
    balance = Column(Integer)

# 初始化数据库连接
engine = create_engine('mysql+pymysql://user:password@localhost/test_db')
Base.metadata.create_all(engine)  # 创建表(如果不存在)
Session = sessionmaker(bind=engine)

def transfer_money():
    session = Session()
    try:
        # ------------------------ 外层事务开始 ------------------------
        session.begin()  # 显式开启外层事务
        
        # 示例:用户初始余额为100
        account = session.query(Account).filter_by(user_id=1).first()
        if not account:
            account = Account(user_id=1, balance=100)
            session.add(account)
            session.flush()  # 生成初始数据(未提交)

        # ------------------------ 内层事务(保存点) ------------------------
        nested_trans = session.begin_nested()  # 创建保存点
        
        try:
            # 内层操作:扣除50元
            account.balance -= 50
            print("[内层事务] 扣除50元,临时余额:", account.balance)  # 输出50
            
            nested_trans.commit()  # 释放保存点(数据未持久化!)
        except Exception as e:
            print("[内层事务] 回滚:", e)
            nested_trans.rollback()  # 回滚到保存点
            raise  # 将异常抛给外层处理

        # ------------------------ 外层事务继续操作 ------------------------
        try:
            # 外层操作:再扣除30元
            account.balance -= 30
            print("[外层事务] 再扣除30元,临时余额:", account.balance)  # 输出20
            
            session.commit()  # 提交外层事务(真正持久化到数据库)
            print("[外层事务] 提交成功,最终余额:", account.balance)
        except Exception as e:
            print("[外层事务] 回滚:", e)
            session.rollback()  # 回滚整个事务(余额恢复为100)
            raise

    except Exception as e:
        print("[全局] 事务失败:", e)
        session.rollback()  # 安全回滚(防止未处理异常导致事务未关闭)
    finally:
        session.close()

if __name__ == "__main__":
    transfer_money()
```

---

### **代码关键逻辑解析**

#### **1. 事务层级控制**
| **操作**               | **说明**                                                                 |
|------------------------|--------------------------------------------------------------------------|
| `session.begin()`       | 显式开启外层事务(等价于 `BEGIN` 语句)。                                 |
| `session.begin_nested()`| 创建内层保存点(等价于 `SAVEPOINT`)。                                    |
| `nested_trans.commit()` | **释放保存点**(内层操作临时生效,但需外层提交后持久化)。                |
| `session.commit()`      | **提交外层事务**,所有保存点内的操作最终持久化到数据库。                  |

#### **2. 执行结果示例**
- **成功流程**:  
  ```
  [内层事务] 扣除50元,临时余额: 50  
  [外层事务] 再扣除30元,临时余额: 20  
  [外层事务] 提交成功,最终余额: 20  
  ```
  - 数据库最终余额为 **20**- **内层事务失败**:  
  若在内层事务中抛出异常(如余额不足):
  ```
  [内层事务] 回滚: 余额不足  
  [全局] 事务失败: 余额不足  
  ```
  - 外层事务未提交,数据库余额保持 **100**- **外层事务失败**:  
  若在外层事务中抛出异常(如扣款失败):
  ```
  [内层事务] 扣除50元,临时余额: 50  
  [外层事务] 回滚: 扣款失败  
  [全局] 事务失败: 扣款失败  
  ```
  - 外层事务回滚,数据库余额恢复为 **100**---

### **改进后代码的优势**
1. **明确事务边界**- 内层事务通过 `nested_trans` 对象控制,避免误用 `session.commit()`。
2. **安全回滚机制**- 无论内层还是外层抛出异常,均能正确回滚到对应保存点或整个事务。
3. **数据一致性**- 内层事务的提交仅为逻辑标记,最终一致性由外层事务保证。

---

### **适用场景**
- **分阶段提交**:如订单创建(先扣库存,再生成支付记录,最后更新订单状态)。
- **复杂业务回滚**:部分操作失败时,仅撤销关联步骤,保留其他操作(需外层提交确认)。

 

在 TiDB 中,索引与数据行的关联关系主要通过 **行标识符(RowID 或主键值)** 实现。以下从索引结构、存储方式、查询机制和维护策略等方面详细说明:

---

### **1. 索引与数据行的存储结构**
TiDB 的底层存储引擎 TiKV 基于 Key-Value 模型,所有数据(包括索引)均以键值对形式存储:

#### **数据行(主表数据)**
- **Key**:`tablePrefix{tableID}_recordPrefixSep{rowID}`  
  - `tablePrefix`:标识表数据的前缀。  
  - `recordPrefixSep`:分隔符。  
  - `rowID`:数据行的唯一标识(显式主键或隐式 RowID)。  
- **Value**:该行的所有列数据(编码为二进制格式,如 Protocol Buffers)。

#### **索引(主键索引、二级索引)**
- **Key**:`tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue{rowID}`  
  - `indexPrefixSep`:标识索引的前缀。  
  - `indexID`:索引的唯一标识。  
  - `indexedColumnsValue`:索引列的值(按定义顺序拼接)。  
  - `rowID`:对应的数据行标识。  
- **Value**:空或包含部分覆盖列数据(若为覆盖索引)。

---

### **2. 索引与数据行的关联机制**
#### **(1) 主键索引**
- 主键索引的 Key 直接包含主键值,Value 指向完整数据行。  
- **查询流程**1. 通过主键值计算 Key,直接从 TiKV 读取对应的 Value(数据行)。  
  2. 无需二次查找,效率最高。

#### **(2) 二级索引(唯一索引、普通索引)**
- 二级索引的 Key 包含索引列的值和对应的行 ID(RowID 或主键值)。  
- **查询流程**(以非覆盖索引为例):  
  1. 根据索引列的值定位到索引条目,获取行 ID。  
  2. 通过行 ID 回表(回主表)查询完整数据行。  
  3. 若为覆盖索引,索引 Value 包含所需列数据,无需回表。

---

### **3. 索引与数据行的维护一致性**
TiDB 通过事务机制保证索引与数据行的 **原子性修改**:

#### **(1) 数据写入(Insert/Update/Delete)**
- **插入数据**1. 写入数据行 Key-Value。  
  2. 写入所有相关索引的 Key-Value(包含行 ID)。  
- **更新数据**1. 删除旧数据行和旧索引条目(逻辑删除,标记为旧版本)。  
  2. 写入新数据行和新索引条目。  
- **删除数据**1. 删除数据行 Key-Value。  
  2. 删除所有相关索引条目。

#### **(2) 多版本并发控制(MVCC)**
- 每个修改操作生成一个新版本数据,索引条目通过版本号(Timestamp)关联对应数据行版本。  
- 查询时根据事务的起始时间戳(Start TS)读取对应版本的数据和索引。

---

### **4. 分布式环境下的索引管理**
#### **(1) 数据与索引的分片(Region)**
- 数据行和其对应的索引条目可能分布在不同的 Region(分片)中。  
- 例如:  
  - 主表数据按主键范围分片到多个 Region。  
  - 索引按索引列值的哈希或范围分片到不同 Region。

#### **(2) 查询优化(避免跨节点访问)**
- TiDB 优化器会尽量将索引查询下推到存储层(TiKV),减少网络开销。  
- 若索引和数据行在同一 Region,可直接本地读取;否则需跨 Region 访问,但通过 Raft 协议保证一致性。

---

### **5. 示例:索引查询流程**
假设表 `users` 结构如下:
```sql
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    INDEX idx_age (age)
);
```

#### **场景:查询 `age = 30` 的用户**
1. **索引扫描**- 通过索引 `idx_age` 的 Key `table_xxx_index_xxx_30_<rowID>` 找到所有 `age=30` 的行 ID。  
2. **回表查询**- 根据行 ID 从主表读取 `id` 和 `name` 列数据。  
3. **返回结果**:合并索引列和回表数据。

---

### **6. 关键特点总结**
| **维度**         | **说明**                                                                 |
|------------------|-------------------------------------------------------------------------|
| **关联纽带**     | 行 ID(RowID 或主键值)是索引与数据行的桥梁。                            |
| **存储分离**     | 索引和数据行独立存储,但通过事务保证一致性。                             |
| **查询效率**     | 主键索引无需回表;二级索引需回表(覆盖索引除外)。                       |
| **分布式支持**   | 索引和数据分片存储,通过 TiDB 优化器和 TiKV 协同减少跨节点访问。          |
| **事务一致性**   | 通过 MVCC 和两阶段提交(2PC)保证索引与数据的原子性。                    |

---

### **7. 对比传统数据库(如 MySQL)**
| **特性**         | **TiDB**                              | **MySQL(InnoDB)**                 |
|------------------|---------------------------------------|--------------------------------------|
| **存储引擎**     | 分布式键值存储(TiKV)                | 本地 B+ 树存储                       |
| **索引分片**     | 支持跨 Region 分布                    | 单机存储,无分片                     |
| **回表机制**     | 需跨节点访问(可能)                  | 本地磁盘访问                         |
| **事务扩展性**   | 支持跨节点分布式事务                  | 单机事务,通过主从复制扩展           |

---

### **总结**
在 TiDB 中,索引通过 **行 ID** 与数据行关联,利用分布式键值存储的特性,实现高效查询和数据一致性。无论是主键索引还是二级索引,均通过事务机制保证操作的原子性,同时优化器与存储层协同工作,减少分布式环境下的性能损耗。理解这一机制有助于合理设计表结构和索引策略,提升 TiDB 应用的性能。


在 TiDB 中,数据(包括索引)的存储最终会以 **Key-Value 键值对** 的形式持久化到 TiKV 节点的硬盘上,但这些数据是经过编码的二进制文件,无法直接通过文本编辑器查看。不过,可以通过以下方法验证索引与数据行的物理存储关系:

---

### **1. 通过 TiDB 系统表和工具查看存储结构**
#### **(1) 查看表的 Region 分布**
每个表的数据和索引会分布在不同的 **Region(分片)** 中,通过以下 SQL 查询表的 Region 信息:
```sql
-- 查询表的 Region 分布信息(替换 {table_name} 为实际表名)
SELECT 
    REGION_ID, 
    START_KEY, 
    END_KEY, 
    PEERS 
FROM 
    INFORMATION_SCHEMA.TIKV_REGION_STATUS 
WHERE 
    TABLE_NAME = '{table_name}';
```

- **输出示例**:
  ```plaintext
  +-----------+-------------------+-----------------+-----------------------+
  | REGION_ID | START_KEY         | END_KEY         | PEERS                 |
  +-----------+-------------------+-----------------+-----------------------+
  | 1001      | t_1234_r          | t_1234_r_999    | [store1, store2, ...] |
  | 1002      | t_1234_i_1_30     | t_1234_i_1_60   | [store3, store4, ...] |
  +-----------+-------------------+-----------------+-----------------------+
  ```
  - `t_1234_r`:主表数据(前缀 `t_{tableID}_r`)。  
  - `t_1234_i_1`:索引数据(前缀 `t_{tableID}_i_{indexID}`)。  
  - `30`、`60`:索引列的值(具体编码格式需解析)。

---

#### **(2) 使用 `pd-ctl` 工具查看 Region 详情**
通过 PD(Placement Driver)控制台工具 `pd-ctl` 查看 Region 的物理存储信息:
```bash
# 进入 pd-ctl 控制台
pd-ctl -u http://{pd-ip}:{pd-port}

# 查看 Region 1001 的详细信息(替换为实际 Region ID)
region 1001
```
- **输出示例**:
  ```json
  {
    "id": 1001,
    "start_key": "7480000000000000FF325F72800000000000000FA",
    "end_key": "7480000000000000FF325F72800000000000000FB",
    "peers": [{"id": 1001, "store_id": 1}],
    "leader": {"id": 1001, "store_id": 1}
  }
  ```
  - `start_key` 和 `end_key` 是经过编码的 Key,需进一步解析。

---

### **2. 通过 TiKV 底层文件验证**
#### **(1) 定位 TiKV 数据目录**
TiKV 的数据存储在配置项指定的目录(默认 `data-dir = "/var/lib/tikv"`),目录结构如下:
```plaintext
/var/lib/tikv
├── raft
├── snap
└── db
    ├── 000001.sst
    ├── 000002.sst
    └── CURRENT
```
- **SST 文件**:存储实际的 Key-Value 数据(Sorted String Table 格式)。  
- **CURRENT 文件**:记录当前活跃的 SST 文件列表。

---

#### **(2) 使用 `tikv-ctl` 工具解析 SST 文件**
通过 `tikv-ctl` 导出 SST 文件的原始 Key-Value 数据(需在 TiKV 节点操作):
```bash
# 导出指定 Region 的 SST 文件数据(替换 {region_id} 和 {sst_file})
tikv-ctl --data-dir /var/lib/tikv/db ldb --hex --region={region_id} scan --from='' --to='z' --path {sst_file}
```

- **输出示例**:
  ```plaintext
  7480000000000000FF325F72800000000000000FA : 0100000000000000FAF9F8F7...
  7480000000000000FF325F72800000000000000FB : 0200000000000000FBFAF9F8...
  ```
  - **Key 解析**- `74800000`:固定前缀,标识 TiKV 内部元数据。  
    - `FF325F72`:Table ID 的十六进制编码(需转换为十进制)。  
    - `8000000000000000FA`:行 ID(RowID)或索引值。  

---

### **3. 编码规则解析**
#### **(1) Key 的编码结构**
TiDB 的 Key 采用以下格式(十六进制):
```plaintext
t{table_id}_[r|i{index_id}]_{encoded_column_value}_{row_id}
```
- **示例**- 主表数据:`t1234_r_00000001` → 表 ID=1234,行 ID=1- 索引数据:`t1234_i2_616C696365_00000001` → 表 ID=1234,索引 ID=2,索引列值="alice",行 ID=1。

#### **(2) Value 的编码结构**
- **主表数据**:列数据按行编码(如 Protocol Buffers)。  
- **索引数据**:若为覆盖索引,包含部分列数据;否则为空或仅含行 ID。

---

### **4. 实验演示:查看表的主键与索引存储**
#### **步骤 1:创建测试表**
```sql
CREATE TABLE test (
    id INT PRIMARY KEY,
    name VARCHAR(20),
    INDEX idx_name (name)
);
```

#### **步骤 2:插入数据**
```sql
INSERT INTO test VALUES (1, 'Alice'), (2, 'Bob');
```

#### **步骤 3:查询 Region 分布**
```sql
SELECT 
    REGION_ID, 
    START_KEY, 
    END_KEY 
FROM 
    INFORMATION_SCHEMA.TIKV_REGION_STATUS 
WHERE 
    TABLE_NAME = 'test';
```
- 假设输出主表 Region ID 为 `2001`,索引 Region ID 为 `2002`。

#### **步骤 4:解析主表 Region 的 Key**
通过 `pd-ctl` 或 `tikv-ctl` 导出 Region 2001 的 Key,解码后应包含:
- 主键 `id=1` → Key `t{test_table_id}_r_1`  
- 主键 `id=2` → Key `t{test_table_id}_r_2`

#### **步骤 5:解析索引 Region 的 Key**
导出 Region 2002 的 Key,解码后应包含:
- 索引 `name='Alice'` → Key `t{test_table_id}_i_{index_id}_Alice_1`  
- 索引 `name='Bob'` → Key `t{test_table_id}_i_{index_id}_Bob_2`

---

### **5. 总结**
- **物理存储可见性**:TiDB 的索引和数据确实以键值对形式存储在硬盘的 SST 文件中,但需通过工具解析编码格式。  
- **验证方法**1. 使用系统表 `INFORMATION_SCHEMA.TIKV_REGION_STATUS` 查看逻辑分布。  
  2. 通过 `pd-ctl` 和 `tikv-ctl` 工具解析底层文件。  
- **核心关联**:索引 Key 中保存了行 ID(或主键值),通过该值回表查询数据行。

通过以上操作,可以实际观察到 TiDB 索引与数据行在物理存储上的关联关系。

 

posted on 2025-05-03 15:26  吃草的青蛙  阅读(50)  评论(0)    收藏  举报

导航