springBoot使用ShardingJDBC实现分表

ShardingSphere的介绍

ShardingSphere是一款起源于当当网内部的应用框架。2015年在当当网内部诞 生,最初就叫ShardingJDBC。2016年的时候,由其中一个主要的开发人员张亮, 带入到京东数科,组件团队继续开发。在国内历经了当当网、电信翼支付、京东数 科等多家大型互联网企业的考验,在2017年开始开源。并逐渐由原本只关注于关系 型数据库增强工具的ShardingJDBC升级成为一整套以数据分片为基础的数据生态 圈,更名为ShardingSphere。到2020年4月,已经成为了Apache软件基金会的顶 级项目。 ShardingSphere包含三个重要的产品,ShardingJDBC、ShardingProxy和 ShardingSidecar。其中sidecar是针对service mesh定位的一个分库分表插件,目 前在规划中。而我们今天学习的重点是ShardingSphere的JDBC这个组 件。 其中,ShardingJDBC是用来做客户端分库分表的产品,而ShardingProxy是用 来做服务端分库分表的产品。这两者定位有什么区别呢?我们看下官方资料中给出 的两个重要的图:

 

 

 

 

1核心概念

分库分表

分库,显而易见,就是一个数据库分成多个数据库,部署到不同机器。

分表,就是一个数据库表分成多个表。

分片

一般在提到分库分表的时候,大多是以水平切分模式(水平分库、分表)为基础来说的,

数据分片将原本一张数据量较大的表例如 t_order 拆分生成数个表结构完全一致的小数据量表 t_order_0、t_order_1、···、t_order_n,每张表只存储原大表中的一部分数据,当执行一条SQL时会通过 分库策略分片策略 将数据分散到不同的数据库、表内。

87a6703b051db1fc739538e1d1230faa.png

数据节点

数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成,例如上图中 order_db_1.t_order_0、order_db_2.t_order_1 就表示一个数据节点。

逻辑表

逻辑表是指一组具有相同逻辑和数据结构表的总称。

比如将订单表 t_order 拆分成 t_order_0 ···  t_order_9 等 10张表。

此时会发现分库分表以后数据库中已不在有 t_order 这张表,取而代之的是 t_order_n,但在代码中写 SQL 依然按 t_order 来写。此时 t_order 就是这些拆分表的逻辑表。

真实表

真实表也就是上边提到的 t_order_n 数据库中真实存在的物理表。

分片键

用于分片的数据库字段。将 t_order 表分片以后,当执行一条SQL时,通过对字段 order_id 取模的方式来决定,这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是 t_order 表的分片健。

 

 实战

以下演示分表,将逻辑订单表 t_order_info 根据订单id字段拆分成3张真实表(t_order_info_0,t_order_info_1,t_order_info_2)

1.创建订单表

 创建好了同时复制另2张表,总共3张表。

2. 创建springboot微服务项目

新建一个微服务项目,引入sharding包

<dependency>
	<groupId>org.apache.shardingsphere</groupId>
	<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
	<version>5.1.2</version>
</dependency>

增加配置

# 配置信息存储方式:内存
spring.shardingsphere.mode.type=Memory

# 显示运行的sql
spring.shardingsphere.props.sql.show=true
# 数据源配置
spring.shardingsphere.datasource.names=ds0

# 数据源0ds0的配置
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://127.0.0.1:3306/order?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useAffectedRows=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456

# 表t_order_info
# 分表规则(由于只分表, 不分库, 所以分库直接采用默认的即可)
# 行内表达式分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.order-inline.type=INLINE
# id列的值%3,等于1就选t_order_info_0表,等于2就选t_order_info_1表,等于3就选t_order_info_2表
spring.shardingsphere.rules.sharding.sharding-algorithms.order-inline.props.algorithm-expression=t_order_info_$->{id % 3}
# 真实表分布,order库的t_order_info_0,t_order_info_1,t_order_info_2表
spring.shardingsphere.rules.sharding.tables.t_order_info.actual-data-nodes=ds0.t_order_info_$->{0..2}
# 以id作为算法的数据值
spring.shardingsphere.rules.sharding.tables.t_order_info.table-strategy.standard.sharding-column=id
# 指定分片算法名称(自定义的名称)
spring.shardingsphere.rules.sharding.tables.t_order_info.table-strategy.standard.sharding-algorithm-name=order-inline

3.生成代码

使用代码生成器生成代码,订单实体OrderInfo如下,其中自己创建了一个@Sharding 注解用来标识分片键,这里根据id值来分表。

@Getter
@Setter
@ToString
@NoArgsConstructor
public class OrderInfo {

	/** 主键ID**/
	@Sharding
    @JsonFormat(shape = Shape.STRING)
	private Long id;

	/** 订单编码**/
	private String orderCode;

	/** 订单时间**/
	private Date orderDate;

	/** 订单状态**/
	private Integer orderStatus;
}

@Sharding 注解内容如下

/**
 * 数据分片
 */
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sharding {

    /**
     * 要识别的信息
     */
    String name() default "";

}

OrderInfoMapper.xml 如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cdw.modules.order.mapper.OrderInfoMapper">

	<resultMap id="OrderInfoMap" type="OrderInfo">
		<id column="id" property="id"/>
	</resultMap>

	<sql id="baseColumns">
		 o.id,
		 o.order_code,
		 o.order_date,
		 o.stop_time,
		 o.order_status
	</sql>

	<!-- 根据主键ID查询 -->
	<select id="findById" resultMap="OrderInfoMap">
		SELECT
		 <include refid="baseColumns" />
		FROM t_order_info o WHERE o.id = #{id}
	</select>

	<!-- 列表  -->
	<select id="findList" resultMap="OrderInfoMap">
		SELECT
		<include refid="baseColumns" />
        FROM t_order_info o
	    <if test="id!=null">AND o.id = #{id}</if>
        <if test="orderCode!=null and orderCode!=''">AND o.order_code LIKE concat('%', #{orderCode}, '%')</if>
        <if test="orderDate!=null">AND o.order_date = #{orderDate}</if>
        <if test="orderStatus!=null">AND o.order_status = #{orderStatus}</if>
        
	</select>

	<!-- 批量插入记录 -->
	<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id" parameterType="OrderInfo">
		INSERT INTO t_order_info (
		 id,
		 order_code,
		 order_date,
		 order_status
		) VALUES
		<foreach  item="emp" separator="," collection="list">
		(
		 #{emp.id},
		 #{emp.orderCode},
		 #{emp.orderDate},
		 #{emp.orderStatus}
		)
	    </foreach>
	</insert>

	<!-- 批量更新记录 -->
	<update id="updateBatch">
		<foreach  item="emp" separator=";" collection="list">
		UPDATE t_order_info
		<set>
			<if test="emp.orderCode!=null and emp.orderCode!=''">order_code = #{emp.orderCode},</if>
			<if test="emp.orderDate!=null">order_date = #{emp.orderDate},</if>
			<if test="emp.orderStatus!=null">order_status = #{emp.orderStatus},</if>
		</set>
		WHERE id = #{emp.id}
		</foreach>
	</update>

	<!-- 批量删除记录 -->
	<delete id="delByIds" parameterType = "java.util.List">
		DELETE FROM t_order_info WHERE
	    id in
		<foreach collection="list"  item="item" open="(" separator="," close=")"  >
		    #{item}
		 </foreach>
	</delete>

</mapper>

OrderInfoService 代码

/**
 * 获取基础订单信息分页列表
 *
 * @author lockie
 * @date: 2022-8-10 09:20
 * @param: param
 */
public PageInfo<OrderInfoVO> getOrderInfoPage(OrderInfo param) {
	param.enablePage();
	return toPageInfo(orderInfoMapper.findList(param), OrderInfoVO.class);
}

/** 查询列表**/
public List<OrderInfoVO> getOrderInfoList(OrderInfo param) {
	 return copyToList(orderInfoMapper.findList(param), OrderInfoVO.class);
}

/** 根据ID获取详情**/
public OrderInfoVO getOrderInfoById(Long id) {
	return orderInfoMapper.findById(id);
}

/** 新增**/
public Integer insertOrderInfo(OrderInfo orderInfo) {
	return insertBatchOrderInfo(Collections.singletonList(orderInfo));
}

/** 批量新增**/
public Integer insertBatchOrderInfo(List<OrderInfo> orderInfoList) {
	if (CollectionUtils.isEmpty(orderInfoList)) {
		return null;
	}
	try {
		List<List<OrderInfo>> data = ShardingUtil.shardingData(shardingCount.getShardingTableCount(), orderInfoList);
		data.forEach(s -> {
			log.info("orderInfoMapper.insertBatchOrderInfo:[{}]", JsonUtil.toJson(s));
			orderInfoMapper.insertBatch(s);
		});
	} catch (Exception e) {
		throw new RuntimeException(e);
	}
	return 1;
}

其中新增操作时重要的就是新增到哪个表去,ShardingUtil就是根据id%3 取余然后找到对应的表

/**
 * 分表工具类
 * 批量操作ShardingSphere本就不支持, 因此封装这个工具用于处理该问题
 * 该工具仅仅支持取模模式
 * @author lockie
 * @date: 2022-7-30 19:09
 */
@Slf4j
public class ShardingUtil {

    private static String fieldName = "";

    private static PropertyDescriptor sourceProperty = null;

    /**
     * 根据数据大小以及分片数量得到初始化分片桶
     *
     * @author lockie
     */
    private static <T> List<List<T>> init(int listCount) {
        List<List<T>> retList = new ArrayList<>(listCount);
        // 默认先占位
        for (int i = 0; i < listCount; i++) {
            retList.add(new ArrayList<>());
        }
        return retList;
    }

    /**
     * 数据分片
     *
     * @author lockie
     * @date: 2022-9-6 14:37
     */
    public static <T> List<List<T>> shardingData(int tableCount, List<T> data) throws RuntimeException, InvocationTargetException, IllegalAccessException {
        if (tableCount == 0) {
            throw new RuntimeException("tableCount 不能为0!");
        }
        List<List<T>> retData = init(tableCount);
        // 如果是1的话, 则表示没有分表, 直接封装数据返回即可
        if (tableCount == 1) {
            retData.set(0, data);
            retData.removeIf(CollectionUtils::isEmpty);
            log.info("处理 retData 返回值3{}", JsonUtil.toJson(retData));
            return retData;
        }
    
        // 基础类型标记
        boolean isBasicType = false;
        // 获取类
        Class<?> clazz = data.get(0).getClass();
        // 判断是否为基本类型的包装类
        if (clazz.getSimpleName().equals("Long") || clazz.getSimpleName().equals("Integer")) {
            isBasicType = true;
        }
        // 标记是否有分表操作
        boolean hasAnnotation = false;
        // 获取类型
        for (T t : data) {
            Object value = null;
            if (!isBasicType) {
                // 检验字段名称是否有值, 没有的话循环一遍属性查找
                if (StringUtils.isEmpty(fieldName)) {
                    Class<?> tClass = t.getClass();
                    List<Field> fields = new ArrayList<>();
                    while (tClass != null) {
                        fields.addAll(Arrays.asList(tClass.getDeclaredFields()));
                        tClass = tClass.getSuperclass();
                    }
                    for (Field field : fields) {
                        Sharding sharding = field.getAnnotation(Sharding.class);
                        if (null == sharding) {
                            continue;
                        }
                        fieldName = field.getName();
                        hasAnnotation = true;
                        break;
                    }
                }
                // 如果没标注分表注解的话, 则默认将原始数据封装好丢出去
                if (!hasAnnotation) {
                    retData.add(data);
                    retData.removeIf(CollectionUtils::isEmpty);
                    log.info("处理 retData 返回值2{}", JsonUtil.toJson(retData));
                    return retData;
                }
    
                // 根据字段名称获取这个属性
                sourceProperty = BeanUtils.getPropertyDescriptor(clazz, fieldName);
                value = sourceProperty.getReadMethod().invoke(t);
            } else {
                value = t;
            }
            // 取模操作
            int index = (int) (Long.parseLong(String.valueOf(value)) % tableCount);
            List<T> valueList = retData.get(index);
            // 添加到对应的List中
            valueList.add(t);
        }

        // 重置状态
        sourceProperty = null;
        fieldName = "";

        // 删除空List
        retData.removeIf(CollectionUtils::isEmpty);
        log.info("处理 retData 返回值1{}", JsonUtil.toJson(retData));
        return retData;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值