自定义策略
类型都是CLASS_BASED。
单分片键
采用一个分片键,这种策略被称为standard
user_database_hash_mod:
type: CLASS_BASED
props:
sharding-count: 32
table-sharding-count: 16
strategy: standard
algorithmClassName: org.opengoofy.index12306.framework.starter.database.algorithm.sharding.CustomDbHashModShardingAlgorithm
具体的自定义实现如下:
public final class CustomDbHashModShardingAlgorithm implements StandardShardingAlgorithm<Comparable<?>> {
private static final String SHARDING_COUNT_KEY = "sharding-count";
private static final String TABLE_SHARDING_COUNT_KEY = "table-sharding-count";
private int shardingCount;
private int tableShardingCount;
@Override
public void init(final Properties props) {
shardingCount = getShardingCount(props);
tableShardingCount = getTableShardingCount(props);
}
@Override
public String doSharding(final Collection<String> availableTargetNames, final PreciseShardingValue<Comparable<?>> shardingValue) {
String suffix = String.valueOf(hashShardingValue(shardingValue.getValue()) % shardingCount / tableShardingCount);
return ShardingAutoTableAlgorithmUtil.findMatchedTargetName(availableTargetNames, suffix, shardingValue.getDataNodeInfo()).orElse(null);
}
@Override
public Collection<String> doSharding(final Collection<String> availableTargetNames, final RangeShardingValue<Comparable<?>> shardingValue) {
return availableTargetNames;
}
private int getShardingCount(final Properties props) {
ShardingSpherePreconditions.checkState(props.containsKey(SHARDING_COUNT_KEY), () -> new ShardingAlgorithmInitializationException(getType(), "Sharding count cannot be null."));
return Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY));
}
private int getTableShardingCount(final Properties props) {
ShardingSpherePreconditions.checkState(props.containsKey(TABLE_SHARDING_COUNT_KEY), () -> new ShardingAlgorithmInitializationException(getType(), "Table sharding count cannot be null."));
return Integer.parseInt(props.getProperty(TABLE_SHARDING_COUNT_KEY));
}
private long hashShardingValue(final Object shardingValue) {
return Math.abs((long) shardingValue.hashCode());
}
@Override
public String getType() {
return "CLASS_BASED";
}
}
复合分片
举个例子,订单服务中t_order的分片就是复合分片,使用的是userid和orderid。当创建订单时,我们通过基因法将用户id的后六位将融入orderid中。同时在复合算法中,我们使用的是id的后六位取hash,这其中包含userid或orderid。我们借此可以通过userid和orderid查询订单。
这种策略被称为complex。
order_database_complex_mod:
type: CLASS_BASED
props:
algorithmClassName: com.zzys.railway.biz.orderservice.dao.algorithm.OrderCommonDataBaseComplexAlgorithm
sharding-count: 32
table-sharding-count: 16
strategy: complex
/**
* 订单数据库复合分片算法配置
*
*/
public class OrderCommonDataBaseComplexAlgorithm implements ComplexKeysShardingAlgorithm {
@Getter
private Properties props;
private int shardingCount;
private int tableShardingCount;
private static final String SHARDING_COUNT_KEY = "sharding-count";
private static final String TABLE_SHARDING_COUNT_KEY = "table-sharding-count";
@Override
public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
Map<String, Collection<Comparable<Long>>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
if (CollUtil.isNotEmpty(columnNameAndShardingValuesMap)) {
String userId = "user_id";
Collection<Comparable<Long>> customerUserIdCollection = columnNameAndShardingValuesMap.get(userId);
if (CollUtil.isNotEmpty(customerUserIdCollection)) {
String dbSuffix;
Comparable<?> comparable = customerUserIdCollection.stream().findFirst().get();
if (comparable instanceof String) {
String actualUserId = comparable.toString();
dbSuffix = String.valueOf(hashShardingValue(actualUserId.substring(Math.max(actualUserId.length() - 6, 0))) % shardingCount / tableShardingCount);
} else {
dbSuffix = String.valueOf(hashShardingValue((Long) comparable % 1000000) % shardingCount / tableShardingCount);
}
result.add("ds_" + dbSuffix);
} else {
String orderSn = "order_sn";
String dbSuffix;
Collection<Comparable<Long>> orderSnCollection = columnNameAndShardingValuesMap.get(orderSn);
Comparable<?> comparable = orderSnCollection.stream().findFirst().get();
if (comparable instanceof String) {
String actualOrderSn = comparable.toString();
dbSuffix = String.valueOf(hashShardingValue(actualOrderSn.substring(Math.max(actualOrderSn.length() - 6, 0))) % shardingCount / tableShardingCount);
} else {
dbSuffix = String.valueOf(hashShardingValue((Long) comparable % 1000000) % shardingCount / tableShardingCount);
}
result.add("ds_" + dbSuffix);
}
}
return result;
}
@Override
public void init(Properties props) {
this.props = props;
shardingCount = getShardingCount(props);
tableShardingCount = getTableShardingCount(props);
}
private int getShardingCount(final Properties props) {
ShardingSpherePreconditions.checkState(props.containsKey(SHARDING_COUNT_KEY), () -> new ShardingAlgorithmInitializationException(getType(), "Sharding count cannot be null."));
return Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY));
}
private int getTableShardingCount(final Properties props) {
ShardingSpherePreconditions.checkState(props.containsKey(TABLE_SHARDING_COUNT_KEY), () -> new ShardingAlgorithmInitializationException(getType(), "Table sharding count cannot be null."));
return Integer.parseInt(props.getProperty(TABLE_SHARDING_COUNT_KEY));
}
private long hashShardingValue(final Comparable<?> shardingValue) {
return Math.abs((long) shardingValue.hashCode());
}
@Override
public String getType() {
return "CLASS_BASED";
}
}
用户服务
t_user_x
表设计
id:雪花算法。
username:全局唯一。
password:密码,使用bcrypt加密。
real_name,region,id_type,id_card,phone,telephone,mail,post_code,address:一些用户信息。
user_type:旅客类型(残疾,军人)
verify_status:审核状态(可能有风险)
deletion_time:注销时间戳
分库分表
t_user:
actualDataNodes: ds_${0..1}.t_user_${0..31}
databaseStrategy:
standard:
shardingColumn: username
shardingAlgorithmName: custom_database_hash_mod # username's hashcode
tableStrategy:
standard:
shardingColumn: username
shardingAlgorithmName: common_table_hash_mod
#common_table_hash_mod:
# type: HASH_MOD
# props:
# sharding-count: 32
分库分表都是使用用户名进行分片
t_user_phone_x&t_user_mail_x
表设计
这两个表起到一个路由表的作用,在下文分库分表策略中会详细的讲解。
phone:
- id
- username
- phone
- deletion_time
mail:
- id
- username
- deletion_time
分库分表
由于我们的应用支持手机号,邮箱,用户名三方登录,但是我们是按照用户名对t_user进行分片,我们在当前场景下又不可能使用基因法。所以在这里建立这两个路由表,类似二级索引回表查询。
t_user_mail:
actualDataNodes: ds_${0..1}.t_user_mail_${0..31}
databaseStrategy:
standard:
shardingColumn: mail
shardingAlgorithmName: custom_database_hash_mod # mail's hashcode
tableStrategy:
standard:
shardingColumn: mail
shardingAlgorithmName: common_table_hash_mod
t_user_phone:
actualDataNodes: ds_${0..1}.t_user_phone_${0..31}
databaseStrategy:
standard:
shardingColumn: phone
shardingAlgorithmName: custom_database_hash_mod # phone's hashcode
tableStrategy:
standard:
shardingColumn: phone
shardingAlgorithmName: common_table_hash_mod
#common_table_hash_mod:
# type: HASH_MOD
# props:
# sharding-count: 32
t_passenger_x
表设计
一个用户可以多名乘车人,可以用来买票时选择多人购票。用户表和乘车人表之间通过用户名进行关联,一对多的关联关系。
- id
- username
- real_name
- id_type,id_card
- discount_type:优惠类型
- phone
- create_date
- verify_status
分库分表
和t_user表是相同的。
信息加密
我们对关键信息进行加密,如文件所示。
tables:
t_user:
columns:
id_card:
cipherColumn: id_card
encryptorName: common_encryptor
phone:
cipherColumn: phone
encryptorName: common_encryptor
mail:
cipherColumn: mail
encryptorName: common_encryptor
address:
cipherColumn: address
encryptorName: common_encryptor
t_passenger:
columns:
id_card:
cipherColumn: id_card
encryptorName: common_encryptor
phone:
cipherColumn: phone
encryptorName: common_encryptor
queryWithCipherColumn: true
encryptors:
common_encryptor:
type: AES
props:
aes-key-value: 123
订单服务
t_order_x
表设计
订单表是订单的核心表,主要描述各个子订单的相同部分,注意多个乘车人除了可以在不同车厢、不同座位类型,其余的都相同,其他核心字段如下:
id:雪花算法+通过基因法融合userid后六位。
order_sn:订单号,基因法融合用户id后六位。
user_id,user_name
train_id,train_number:1,G35
riding_date:乘车日期,精确到日
departure,arrival:出发,到达站点
source,status:订单来源,订单状态(已支付,已退款,待支付)
pay_type,pay_time:支付方式,支付时间。
分库分表
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..31}
databaseStrategy:
complex:
shardingColumns: user_id,order_sn
shardingAlgorithmName: order_database_complex_mod
tableStrategy:
complex:
shardingColumns: user_id,order_sn
shardingAlgorithmName: order_table_complex_mod
分片采用复合分片算法,可以提供两个维度的查询:通过用户id查询订单,通过订单号查询订单。
t_order_item_x
表设计
订单详情表是订单表中每个乘车人乘坐的详情信息,核心字段如下:
id:雪花算法
order_sn:订单号,和t_order_x相同
user_id,user_name,real_name
id_type,id_card,phone:证件类型,证件号,手机号
train_id,carriage_number:1,01号车厢
seat_type,seat_number:
0:商务座,1:一等座,2:二等座
,05A号座位status,amount:订单状态,订单金额
分库分表
和t_order相同。
t_order_passenger_x
表设计
订单和乘车人的连表,有可能乘车人在乘车时并没有自己的账号,如果乘车人在注册自己的账号之后,应该能够查到自己的订单。但是由于分片的存在,用户id和订单id为复合分片键,我们很难通过t_order_item获取到相同的id_card的所有订单(读扩散)。当我们创建这个路由表(冗余表)后,再根据id_card分片,这样就可以满足要求。
id:雪花算法
order_sn:订单号
id_type,id_card:证件类型,证件号
分库分表
如上述,我们采用id_card为分片键
t_order_item_passenger:
actualDataNodes: ds_${0..1}.t_order_item_passenger_${0..31}
databaseStrategy:
standard:
shardingColumn: id_card
shardingAlgorithmName: order_passenger_relation_database_mod
tableStrategy:
standard:
shardingColumn: id_card
shardingAlgorithmName: order_passenger_relation_table_mod
信息加密
t_order_item:
columns:
id_card:
cipherColumn: id_card
encryptorName: common_encryptor
phone:
cipherColumn: phone
encryptorName: common_encryptor
queryWithCipherColumn: true
encryptors:
common_encryptor:
type: AES
props:
aes-key-value: d6oadClrrb9A3GWo
支付服务
t_pay_x
表设计
订单表涉及的字段较多,大部分是和支付平台之间的交互,其中又包含了很多序列号。
- id:雪花算法
- pay_sn:支付流水号,雪花算法
- order_sn:订单号
- out_order_sn:支付平台提供的订单号
- channel:支付渠道(支付宝,微信)
- trade_type:支付环境(APP,网页)
- subject:订单标题(支付平台显示)
- order_request_id:商户请求号(由商户定义需要保证不重复)
- total_amount:交易总金额
- trade_no:三方交易凭证号(支付平台提供)
- gmt_payment:付款时间
- pay_amount:付款金额
- status:支付状态
分库分表
t_pay:
actualDataNodes: ds_${0..1}.t_pay_${0..31}
databaseStrategy:
complex:
shardingColumns: order_sn,pay_sn
shardingAlgorithmName: pay_database_complex_mod
tableStrategy:
complex:
shardingColumns: order_sn,pay_sn
shardingAlgorithmName: pay_table_complex_mod
和订单表的逻辑相同,同样采用order_sn,pay_sn复合分片,支持多维度查询。
票务服务
票务服务中大部分表的数据量不会很大,每天的票务是通过定时任务刷新,所以没有涉及到分库分表。
t_region
地区表,基本上是以城市为单位。
- id:雪花算法
- name:北京
- full_name:北京市
- code:BJP
- initial:B
- spell:beijing
- popular_flag:1(代表是否是热门地区)
t_station
以车站为单位,北京可以又北京南,北京西等车站。
- id
- code:VNP
- name:北京南
- spell:beijingnan
- region:BJP
- region_name:北京
t_train
火车数据表。
start_station,end_station,start_region,end_region实际上是冗余数据,由t_train_station_relation规定。
- id
- train_number:G35
- train_type:列车类型 0:高铁 1:动车 2:普通车
- train_tag:列车标签 0:复兴号 1:智能动车组 2:静音车厢 3:支持选铺
- train_brand:列车品牌类型 0:GC-高铁/城际 1:D-动车 2:Z-直达 3:T-特快 4:K-快速 5:其他 6:复兴号 7:智能动车组
- start_station,end_station:起始站,终点站
- start_region,end_region:起始地区,终点地区
- sale_time,sale_status:销售时间,销售状态
- departure_time,arrival_time:出发时间,到达时间
t_carriage
车厢表
- id
- train_id:关联的列车id
- carriage_number:车厢号(01,02...)
- carriage_type:车厢类型(商务,硬卧,软卧,行李车厢等)
- seat_count:车厢内座位数量
t_seat
座位表,假设有a->b->c路线的一个座位,那么对于一个实际存在的物理座位,实际上会存在三个逻辑座位,价格也不相同。
其中价格由t_train_station_price计算而来,起点终点由t_train_station_relation规定。
- id
- train_id
- carriage_number
- seat_number:座位号(05F)
- seat_type:座位类型
- start_station:起始站
- end_station:终点站
- price:价格
- seat_status:当前状态(可销售,已锁定,已销售)
t_train_station
火车和站点的连表
- id
- train_id
- station_id:出发站点id
- sequence:到站顺序
- departure,arrival:出发站点,到达站点
- start_region,end_region:出发地,到达地
- departure_time,arrival_time:出发时间,到达时间
- stopover_time:停留时间(分钟)
举个例子:北京南->济南南->南京南,那么对于train_id、sequence、departure、arrival就有:
- 1,01,北京南,济南南
- 1,02,济南南,南京南
t_train_station_price
该表规定了所有的逻辑路线的所有类型座位的价格,t_seat的价格就是以此计算。
- id
- train_id
- departure,arrival:起始站点,到达站点
- seat_type
- price
t_train_station_relation
列车站点关系表,该表规定了列车的中途站点
对于北京南到南京南,可能存在多条火车经过,以train_id作为区分
- id
- train_id
- departure,arrival
- start_region,end_region
- departure_flag,arrival_flag:说明着当前这条子线路是否是完整线路的起始站点或终止站点
- departure_time,arrival_time