Skip to content

Commit 5608a46

Browse files
CopilotSoar360
andauthored
feat: add FieldShardPolicy for business-field-based table sharding
Agent-Logs-Url: https://github.com/NewLifeX/NewLife.XCode/sessions/9ea0dca5-2f74-4a06-88fa-ccadd9f32bfd Co-authored-by: Soar360 <15421284+Soar360@users.noreply.github.com>
1 parent c15aabe commit 5608a46

3 files changed

Lines changed: 616 additions & 6 deletions

File tree

Doc/Shards分配策略与路由.md

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# Shards 分配策略与路由(Shards)
22

3-
分表分库是 XCode 处理海量数据的核心能力之一。`XCode.Shards` 提供策略接口 `IShardPolicy` 与内置实现 `TimeShardPolicy`,用于把实体对象/时间/雪花Id/查询表达式映射到目标库表。
3+
分表分库是 XCode 处理海量数据的核心能力之一。`XCode.Shards` 提供策略接口 `IShardPolicy` 与两个内置实现 `TimeShardPolicy`(时间分片)和 `FieldShardPolicy`(业务字段分片),用于把"实体对象/时间/雪花Id/查询表达式"映射到目标库表。
44

55
## 核心对象
66

77
- `IShardPolicy`
8-
- `Shard(Object value)`:单值路由(实体、时间、雪花Id)
8+
- `Shard(Object value)`:单值路由(实体、时间、雪花Id、直接字段值
99
- `Shards(DateTime start, DateTime end)`:按时间区间给出多分片
1010
- `Shards(Expression expression)`:从查询条件推导多分片
1111
- `ShardModel(String ConnName, String TableName)`:目标分片描述
1212
- `TimeShardPolicy`:内置时间分片策略,支持连接名与表名双路由
13+
- `FieldShardPolicy`:内置业务字段分片策略,按任意字段值(如 UserId)路由到对应分表
1314

1415
## 1)策略挂载位置
1516

@@ -115,18 +116,98 @@ Meta.ShardPolicy = new TimeShardPolicy(nameof(Id), Meta.Factory)
115116
};
116117
```
117118

118-
## 9)常见问题
119+
## 9)业务字段分表策略(FieldShardPolicy)
120+
121+
`FieldShardPolicy` 适用于多租户等按业务字段固定路由的场景。整张分表内,该字段的值保持一致。
122+
123+
### 核心特性
124+
125+
- 按任意字段(`Int32`/`Int64`/`String` 等)的**等值**路由
126+
- 写入(Insert/Update/Delete)和等值查询均自动路由到对应分表
127+
- 非等值查询(`>``<` 等)不路由,回落到主表(安全降级)
128+
- 不支持时间区间扫描(`Shards(start, end)` 返回空数组)
129+
130+
### 配置示例
131+
132+
```csharp
133+
// 按 UserId 字段分表,UserId=1000 时表名为 UserLog_1000
134+
Meta.ShardPolicy = new FieldShardPolicy(nameof(UserId), Meta.Factory)
135+
{
136+
TablePolicy = "{0}_{1}",
137+
};
138+
139+
// 同时分库分表
140+
Meta.ShardPolicy = new FieldShardPolicy(nameof(TenantId), Meta.Factory)
141+
{
142+
ConnPolicy = "{0}_{1}",
143+
TablePolicy = "{0}_{1}",
144+
};
145+
```
146+
147+
其中:
148+
- `ConnPolicy`:连接名格式,`{0}` 基础连接名,`{1}` 字段值(如 `{0}_{1}``mydb_1000`
149+
- `TablePolicy`:表名格式,`{0}` 基础表名,`{1}` 字段值(如 `{0}_{1}``Log_1000`
150+
- 默认 `TablePolicy = "{0}_{1}"``ConnPolicy` 为空(不切库)
151+
152+
### 路由输入类型
153+
154+
`FieldShardPolicy.Shard(Object value)` 支持:
155+
156+
- `IModel`:从实体对象读取指定分片字段值
157+
- 任意直接值(`Int32`/`Int64`/`String` 等):直接格式化进策略
158+
159+
### 查询路由
160+
161+
`Shards(Expression expression)` 从查询条件中寻找分片字段的**等值条件**
162+
163+
- `UserId == 1000` → 路由到 `Log_1000`
164+
- `Success == true & UserId == 2000` → 路由到 `Log_2000`
165+
- `UserId > 0`(非等值)→ 返回空,回落主表
166+
- 不含分片字段条件 → 返回空,回落主表
167+
168+
### 推荐配置模板
169+
170+
#### 多租户按 TenantId 分表
171+
172+
```csharp
173+
// 一般在实体类的静态构造函数中配置
174+
static UserLog()
175+
{
176+
Meta.ShardPolicy = new FieldShardPolicy(nameof(TenantId), Meta.Factory)
177+
{
178+
TablePolicy = "{0}_{1}",
179+
};
180+
}
181+
```
182+
183+
#### 多租户按 UserId 分库分表
184+
185+
```csharp
186+
static Log()
187+
{
188+
Meta.ShardPolicy = new FieldShardPolicy(nameof(UserId), Meta.Factory)
189+
{
190+
ConnPolicy = "db_{1}",
191+
TablePolicy = "{0}_{1}",
192+
};
193+
}
194+
```
195+
196+
## 10)常见问题
119197

120198
- **为何查询没走分片?**
121199
- 查询条件里缺少分片字段,或表达式无法提取范围。
122200
- **为何写入落到主表?**
123201
- `ShardPolicy` 未配置,或输入对象分片字段值无效。
124202
- **雪花Id分片报错?**
125203
- Id 不是合法雪花值,无法解析时间。
204+
- **FieldShardPolicy 范围查询不分表?**
205+
- `FieldShardPolicy` 仅支持等值条件路由,范围查询(`>``<`)会回落主表,这是预期行为。
126206

127-
## 10)实践建议
207+
## 11)实践建议
128208

129-
- 分片键必须稳定、可提取区间(推荐时间或雪花Id)。
130-
- `Step` 尽量与分片粒度一致,避免过度扫描。
209+
- 分片键必须稳定、可提取区间(`TimeShardPolicy` 推荐时间或雪花Id,`FieldShardPolicy` 推荐租户/用户Id)。
210+
- `TimeShardPolicy``Step` 尽量与分片粒度一致,避免过度扫描。
211+
- `FieldShardPolicy` 适合整表固定属于某个业务主体的场景(如多租户日志)。
131212
- 批量写入时优先交给框架自动分组,不要手工拆库拆表。
132213
- 分片策略应与数据保留策略(按天/按月清理)统一设计。

XCode/Shards/FieldShardPolicy.cs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using NewLife;
2+
using NewLife.Data;
3+
using XCode.Configuration;
4+
5+
namespace XCode.Shards;
6+
7+
/// <summary>字段值分表策略。按指定业务字段的值分表,如按UserId字段分表,UserId=1000时表名为Log_1000</summary>
8+
/// <remarks>
9+
/// 适用于多租户等场景,整张分表的字段值固定一致。
10+
/// 示例:UserId=1000 → Log_1000
11+
/// </remarks>
12+
public class FieldShardPolicy : IShardPolicy
13+
{
14+
#region 属性
15+
/// <summary>实体工厂</summary>
16+
public IEntityFactory? Factory { get; set; }
17+
18+
/// <summary>字段</summary>
19+
public FieldItem? Field { get; set; }
20+
21+
/// <summary>连接名策略。格式化字符串,0位基础连接名,1位字段值,如{0}_{1}</summary>
22+
public String? ConnPolicy { get; set; }
23+
24+
/// <summary>表名策略。格式化字符串,0位基础表名,1位字段值,如{0}_{1}</summary>
25+
public String? TablePolicy { get; set; } = "{0}_{1}";
26+
27+
private readonly String? _fieldName;
28+
#endregion
29+
30+
#region 构造
31+
/// <summary>实例化</summary>
32+
public FieldShardPolicy() { }
33+
34+
/// <summary>指定字段实例化字段分表策略</summary>
35+
/// <param name="field">分表字段</param>
36+
/// <param name="factory">实体工厂</param>
37+
public FieldShardPolicy(FieldItem field, IEntityFactory? factory = null)
38+
{
39+
Field = field;
40+
Factory = factory ?? field.Factory;
41+
}
42+
43+
/// <summary>指定字段名和工厂实例化字段分表策略</summary>
44+
/// <param name="fieldName">字段名</param>
45+
/// <param name="factory">实体工厂</param>
46+
public FieldShardPolicy(String fieldName, IEntityFactory factory)
47+
{
48+
_fieldName = fieldName;
49+
Factory = factory;
50+
51+
// 异步加载字段
52+
Task.Run(GetField);
53+
}
54+
55+
private FieldItem? GetField() => Field ??= _fieldName == null ? null : Factory?.Table.FindByName(_fieldName);
56+
#endregion
57+
58+
#region 分表
59+
/// <summary>为实体对象或字段值计算分表分库</summary>
60+
/// <param name="value">实体对象或字段直接值(Int32/Int64/String 等)</param>
61+
/// <returns>分表模型,策略未配置时返回 null</returns>
62+
public virtual ShardModel? Shard(Object value)
63+
{
64+
if (value is IModel entity) return ShardByEntity(entity);
65+
return ShardByValue(value);
66+
}
67+
68+
/// <summary>从实体对象中提取分表字段值并计算分表分库</summary>
69+
/// <param name="entity">实体对象</param>
70+
/// <returns>分表模型</returns>
71+
protected virtual ShardModel? ShardByEntity(IModel entity)
72+
{
73+
var fi = GetField() ?? throw new XCodeException("字段分表策略要求指定分表字段!");
74+
75+
var value = entity[fi.Name];
76+
if (value == null) throw new XCodeException($"实体对象字段[{fi.Name}]为空,无法用于字段分表");
77+
78+
return ShardByValue(value);
79+
}
80+
81+
/// <summary>按字段值计算分表分库</summary>
82+
/// <param name="fieldValue">字段值</param>
83+
/// <returns>分表模型,策略未配置时返回 null</returns>
84+
public virtual ShardModel? ShardByValue(Object fieldValue)
85+
{
86+
if (ConnPolicy.IsNullOrEmpty() && TablePolicy.IsNullOrEmpty()) return null;
87+
88+
if (Factory == null) throw new XCodeException("字段分表策略要求指定实体工厂!");
89+
var table = Factory.Table;
90+
91+
var connName = table.ConnName;
92+
var tableName = table.TableName;
93+
if (!ConnPolicy.IsNullOrEmpty()) connName = String.Format(ConnPolicy, connName, fieldValue);
94+
if (!TablePolicy.IsNullOrEmpty()) tableName = String.Format(TablePolicy, tableName, fieldValue);
95+
96+
return new(connName, tableName);
97+
}
98+
99+
/// <summary>字段值分表策略不支持时间区间查询,直接返回空数组</summary>
100+
/// <param name="start">开始时间(不使用)</param>
101+
/// <param name="end">结束时间(不使用)</param>
102+
/// <returns>空数组</returns>
103+
public virtual ShardModel[] Shards(DateTime start, DateTime end) => [];
104+
105+
/// <summary>从查询表达式中提取等值条件计算分表分库</summary>
106+
/// <param name="expression">查询表达式</param>
107+
/// <returns>分表模型数组;条件中没有分表字段时返回空数组</returns>
108+
public virtual ShardModel[] Shards(Expression expression)
109+
{
110+
var fi = GetField() ?? throw new XCodeException("字段分表策略要求指定分表字段!");
111+
112+
var exps = new List<FieldExpression>();
113+
if (expression is WhereExpression where)
114+
exps = where.Where(e => e is FieldExpression fe && fe.Field.Name == fi.Name).Cast<FieldExpression>().ToList();
115+
else if (expression is FieldExpression fe2 && fe2.Field.Name == fi.Name)
116+
exps.Add(fe2);
117+
118+
if (exps.Count == 0) return [];
119+
120+
// 仅支持等值条件
121+
var eq = exps.FirstOrDefault(e => e.Action == "=");
122+
if (eq != null)
123+
{
124+
var model = ShardByValue(eq.Value);
125+
if (model != null) return [model];
126+
}
127+
128+
return [];
129+
}
130+
#endregion
131+
}

0 commit comments

Comments
 (0)