自定义策略

类型都是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
  • mail
  • 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
上次更新:
Contributors: YangZhang