面试题:
什么是分库分表
分库分表是一种数据库优化策略。
目的:为了解决由于单一的库表数据量过大而导致数据库性能降低的问题
分库:将原来独立的数据库拆分成若干数据库组成
分表:将原来的大表(存储近千万数据的表)拆分成若干个小表
什么时候考虑分库分表
注意:当我们使用读写分离、索引、缓存后,数据库的压力还是很大的时候,这就需要将数据库进行拆分(迫不得已,最后最后的办法)。
-
单表出现的瓶颈:单表数据量过大,导致读写性能较慢。 关于 MySQL 单库和单表的数据量限制,和不同的服务器配置,以及不同结构的数据存储有关,并没有一个确切的数字。这里参考阿里巴巴的《Java 开发手册》中数据库部分的建表规约:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
-
单库出现的瓶颈:CPU压力过大,内存不足,磁盘空间不足,网络带宽不足,导致读写性能较慢
分库分表的实现方式
分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库、水平分库、垂直分表、水平分表四种方式。
1.垂直分表
概念:按照字段访问频次,将一张表的字段拆分到多张表当中。
1. 拆分后表的记录行数不变
2. 拆分的表之间一般是一对一的关系
3. 每张表仅存储其中一部分字段
2.垂直分库
概念:按照业务将表进行分类,分布到不同的数据库上面。
核心理念:专库专用
注意:它需要解决跨库带来的所有复杂问题(例如:事务一致性问题)。
3.水平分表
概念:整张表在数据结构不变的情况下,将大表按一定规则拆分成多个小表
1. 每个表的结构一样;
2. 每个表的数据不一样;
3. 所有表的数据并集为全量数据;
算法说明:
如果商品ID为偶数,将此操作映射至商品信息1表;
如果商品ID为奇数,将操作映射至商品信息2表。
此操作要访问表名称的表达式为商品信息[商品ID%2 + 1];
4.水平分库
概念:将表水平切分后 按某个字段的某种规则分到不同的数据库,使得每个库具有相同的表,表中的数据不相同,水平分库一般是伴随水平分表。
场景:
假如当前有8w店铺,每个店铺平均150个不同规格的商品,那商品数量得往1200w+上预估,并且商品库属于访问非常频繁的资源,单台服务器已经无法支撑。此时该如何优化?
目前情况是那怕再次垂直分库也无法解决数据瓶颈问题。我们可以尝试水平分库,将商品ID为奇数的和商品ID为偶数的商品信息分别放在两个不同库中;
说明:
总之,水平分库后,各个库保存的表结构是一致的,但是表中内容不一样;
最佳实践:
一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案。当然在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。
总之,基于开发和维护成本比考虑,非必须,不要对数据库做分库分表处理!
分库分表带来的问题(了解)
单个数据库的时候MySQL操作很简单,但是当进行分库分表之后数据库被拆分的非常细,这个时候如何操作这些表就是要面临的问题,就需要引入sharding-jdbc,来解决分库分表操作的问题,sharding-jdbc不是进行分库分表的,分库分表是已经提前分好的,sharding-jdbc是解决了如何方便的去操作这些数据库表。
-
分布式事务:
-
问题:在分库分表的环境中,一个业务操作可能涉及多个数据库或表,这会导致分布式事务的问题。
-
解决方案:
ShardingSphere
支持多种分布式事务解决方案,如 XA、Seata 等,可以有效地处理跨库事务。 -
全局唯一 ID 生成:
-
问题:在分库分表的情况下,需要生成全局唯一的主键 ID。
-
解决方案:
ShardingSphere
提供了多种 ID 生成策略,如 Snowflake、UUID 等,可以确保生成的 ID 在全局范围内唯一。 -
数据迁移和扩容:
-
问题:随着业务的发展,可能需要对现有的分片规则进行调整或增加新的分片。
-
解决方案:
ShardingSphere
提供了灵活的数据迁移和扩容工具,支持在线数据迁移和重新分片。 -
查询路由:
-
问题:查询请求需要被正确地路由到相应的分片上。
-
解决方案:
ShardingSphere
内置了强大的 SQL 解析和路由引擎,能够自动解析 SQL 并将其路由到正确的分片。 -
聚合查询:
-
问题:跨分片的聚合查询(如
GROUP BY
,ORDER BY
)会变得复杂。 -
解决方案:
ShardingSphere
支持跨分片的聚合查询,并通过内存计算或中间结果集合并来实现高效的聚合。 -
读写分离:
-
问题:为了进一步提高性能,可能需要实现读写分离。
-
解决方案:
ShardingSphere
支持读写分离,可以将读操作路由到从库,写操作路由到主库。 -
配置管理:
-
问题:分库分表的配置较为复杂,需要管理和维护。
-
解决方案:
ShardingSphere
提供了灵活的配置方式,支持 YAML、Spring Boot 配置文件等,方便管理和维护。 -
数据一致性:
-
问题:在分库分表的情况下,数据的一致性难以保证。
-
解决方案:
ShardingSphere
提供了多种一致性保证机制,如最终一致性、强一致性等,可以根据业务需求选择合适的方案。 -
监控和运维:
-
问题:分库分表后的系统监控和运维变得更加复杂。
-
解决方案:
ShardingSphere
提供了丰富的监控和日志功能,支持与 Prometheus、Grafana 等监控系统集成,便于运维。
ShardingSphere
是一个非常强大的框架,能够解决分库分表带来的大部分问题。它提供了全面的解决方案,包括分布式事务、全局唯一 ID 生成、数据迁移和扩容、查询路由、聚合查询、读写分离、配置管理、监控和运维等。虽然 ShardingSphere
可以解决很多问题,但在实际应用中,还需要根据具体的业务需求和场景进行适当的配置和调优。此外,对于一些非常复杂的业务逻辑和特定需求,可能仍需要额外的定制化开发。
Sharding-JDBC入门
1、ShardingSphere生态简介
Apache ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成,既能够独立部署,又支持混合部署配合使用的产品组成。
功能特性:它们均提供标准化的数据水平扩展、分布式事务和分布式治理等功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
概览 :: ShardingSphere (apache.org)
文档地址:https://shardingsphere.apache.org/document/legacy/4.x/document/cn/overview
本教程主要介绍:Sharding-JDBC
Sharding-JDBC
最早是当当网内部使用的一款分库分表框架,到2017年的时候才开始对外开源,这几年在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为 ShardingSphere
,2020年4⽉16⽇正式成为 Apache
软件基⾦会的顶级项⽬。
Sharding-jdbc是ShardingSphere的其中一个模块,定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
-
适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
-
基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
-
支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。
Sharding-JDBC的核心功能为数据分片和读写分离,通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
数据分片:把表和数据库进行拆分
读写分离:把数据库分成主从两个数据库,主数据库负责写,从数据库负责读
2、sharding-jdbc相关名词解释
参考官网-核心概念
-
逻辑表(LogicTable):(面向开发,实际上是不存在的表)进行水平拆分的时候同一类型(逻辑、数据结构相同)的表的总称。
-
真实表(ActualTable):(真实存在的表,真实操作的表)在分片的数据库中真实存在的物理表。
-
数据节点(DataNode):(描述的是哪个数据源下的哪张表,相当与地理位置的定位)数据分片的最小单元。由数据源名称和数据表组成,例:order_db.t_order_1, order_db.t_order_2,order_db.t_order_3 说白了,具体到指定库下的指定表就是一个数据节点;
-
动态表(DynamicTable):(属于真实表,随某种规则动态产生)逻辑表和物理表不一定需要在配置规则中静态配置。如,按照日期分片的场景,物理表的名称随着时间的推移会产生变化(股票流水)。
-
广播表(公共表):(数据量较小,变动少,而且属于高频联合查询的依赖表)指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表:对数据库结构的详细描述、参数表。
-
绑定表(BindingTable):多表之间存在关系,尽量关联的数据放到一个库中,避免跨库查询(让相同字段都参与到分库的规则,就会落到同一个库中)指分片规则一致的主表和子表。例如:
t_order
表和t_order_item
表,均按照order_id
分片,则此两张表互为绑定表关系。 -
分片键(ShardingColumn):分片字段用于将数据库(表)水平拆分的字段,支持单字段及多字段分片。例如上例中的order_id。一般在业务中经常查询使用的字段会作为分片键
3、Sharding-JDBC执行原理
参考官网-内部剖析
4、sharding-jdbc分片方式介绍
sharding-jdbc实现数据分片有4种策略:
1.inline模式
-
使用最简单,开发成本比较低;
-
只能使用单个字段作为分片键;
-
基于行表达式定义分片规则;
通过groovy表达式来表示分库分表的策略:
db0
├── t_order_1
└── t_order_2
db1
├── t_order_1
└── t_order_2
表达式:
db${0..1}.t_order_${1..2} 通过一个表达式代表4个数据节点
t_order_${orderId % 2 + 1 } 表的分片规则
2.standard标准分片模式
-
用户可通过代码自定义复杂的分片策略;
-
同样只能使用单个字段作为分片键;
3.complex复合分片模式
-
用于多分片键的复合分片策略(多片键)
4.Hint强制分片模式
-
不指定片键,通过代码动态指定路由规则
-
强制分片策略(强制路由)
分库分表实战.md
官网
首先我们先进入官网:
Sharding Sphere
【文档】—>【遗留】
进去我们就可以看见它的版本,这里我没有用最新版本,用的是4.X版本,4.X.X的版本和5.X.X的版本核心API变化不大,但是细节上是有改动的,具体可以看官方文档,这里以4.x版本为例,其他的版本就不做过多的赘述,有兴趣可以自行了解
进入到4.x版本内后,根据下图找到我们的Spring Boot配置,里面有很多核心特性。如读写分离、数据脱敏等。。。
【用户手册】—>【Sharding-JDBC】—>【配置手册】—>【Spring Boot配置】
进入里面,我们首先找到配置,如下图。这里的配置都有注释
新建项目
这里用的是jDK1.8
直接创建就好了
进去之后,记得更改一下编码格式,防止配置乱码
【File】—>【Settings】—>【File Encodings】
将以下改为UTF-8格式
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath></relativePath>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--引入sharding依赖-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
<!--连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.3.12.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
实体类
TOrder.java
package com.by.entity;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* @TableName t_order
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TOrder implements Serializable {
/**
* 订单id
*/
private Long orderId;
/**
* 订单价格
*/
private BigDecimal price;
/**01
* 下单用户id
*/
private Long userId;
/**
* 订单状态
*/
private String status;
private static final long serialVersionUID = 1L;
}
TDict.java
package com.by.entity;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* @TableName t_dict
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TDict implements Serializable {
/**
* 字典id
*/
private Long dictId;
/**
* 字典类型
*/
private String type;
/**
* 字典编码
*/
private String code;
/**
* 字典值
*/
private String value;
private static final long serialVersionUID = 1L;
}
TUser.java
package com.by.entity;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* @TableName t_user
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TUser implements Serializable {
/**
* 用户id
*/
private Long userId;
/**
* 用户姓名
*/
private String fullname;
/**
* 用户类型
*/
private String userType;
private static final long serialVersionUID = 1L;
}
TbLog
package com.by.entity;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* @TableName tb_log
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TbLog implements Serializable {
/**
*
*/
private Long id;
/**
*
*/
private String info;
private static final long serialVersionUID = 1L;
}
dao层
TOrderDao.java
package com.by.dao;
import com.by.entity.TOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @Entity com.by.pojo.TOrder
*/
@Mapper
public interface TOrderDao {
int deleteByPrimaryKey(Long id);
int insert(TOrder record);
int insertSelective(TOrder record);
TOrder selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(TOrder record);
int updateByPrimaryKey(TOrder record);
/**
* 根据用户id精准查询,获取订单集合
* @param userId
* @return
*/
List<TOrder> findByUserId(Long userId);
/**
* 根据用户id范围查询对应的订单集合信息
* @param start
* @param end
* @return
*/
List<TOrder> findRangeByUserId(@Param("start") Long start, @Param("end") Long end);
/**
* 根据订单id范围查询
* @param start
* @param end
* @return
*/
List<TOrder> selectByRange(@Param("start") Long start,@Param("end") Long end);
/**
* 根据用户id等值查询,并且根据订单id范围查询
* @param userId
* @param start
* @param end
* @return
*/
List<TOrder> selectByRange2(@Param("userId") Long userId, @Param("start") Long start, @Param("end") Long end);
}
TDictDao.java
package com.by.dao;
import com.by.entity.TDict;
import org.apache.ibatis.annotations.Mapper;
/**
* @Entity com.by.entity.TDict
*/
@Mapper
public interface TDictDao {
int deleteByPrimaryKey(Long id);
int insert(TDict record);
int insertSelective(TDict record);
TDict selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(TDict record);
int updateByPrimaryKey(TDict record);
}
TUserDao.java
package com.by.dao;
import com.by.entity.TUser;
import org.apache.ibatis.annotations.Mapper;
/**
* @Entity com.by.entity.TUser
*/
@Mapper
public interface TUserDao {
int deleteByPrimaryKey(Long id);
int insert(TUser record);
int insertSelective(TUser record);
TUser selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(TUser record);
int updateByPrimaryKey(TUser record);
}
TbLogDao.java
package com.by.dao;
import com.by.entity.TbLog;
import org.apache.ibatis.annotations.Mapper;
/**
* @Entity com.by.entity.TbLog
*/
@Mapper
public interface TbLogDao {
int deleteByPrimaryKey(Long id);
int insert(TbLog record);
int insertSelective(TbLog record);
TbLog selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(TbLog record);
int updateByPrimaryKey(TbLog record);
}
xml文件
TOrderMapper.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.by.dao.TOrderDao">
<resultMap id="BaseResultMap" type="com.by.entity.TOrder">
<id property="orderId" column="order_id" jdbcType="BIGINT"/>
<result property="price" column="price" jdbcType="DECIMAL"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="status" column="status" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
order_id,price,user_id,
status
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_order
where order_id = #{orderId,jdbcType=BIGINT}
</select>
<select id="findByUserId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_order
where user_id = #{userId,jdbcType=BIGINT}
</select>
<select id="findRangeByUserId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_order
where user_id between #{start} and #{end}
</select>
<select id="selectByRange" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_order
where order_id between #{start} and #{end}
</select>
<select id="selectByRange2" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_order
where order_id between #{start} and #{end} and user_id=#{userId}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from t_order
where order_id = #{orderId,jdbcType=BIGINT}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TOrder" useGeneratedKeys="true">
insert into t_order
( order_id,price,user_id
,status)
values (#{orderId,jdbcType=BIGINT},#{price,jdbcType=DECIMAL},#{userId,jdbcType=BIGINT}
,#{status,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TOrder" useGeneratedKeys="true">
insert into t_order
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="orderId != null">orderId,</if>
<if test="price != null">price,</if>
<if test="userId != null">userId,</if>
<if test="status != null">status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="orderId != null">order_id = #{orderId,jdbcType=BIGINT},</if>
<if test="price != null">price = #{price,jdbcType=DECIMAL},</if>
<if test="userId != null">user_id = #{userId,jdbcType=BIGINT},</if>
<if test="status != null">status = #{status,jdbcType=VARCHAR},</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.by.entity.TOrder">
update t_order
<set>
<if test="price != null">
price = #{price,jdbcType=DECIMAL},
</if>
<if test="userId != null">
user_id = #{userId,jdbcType=BIGINT},
</if>
<if test="status != null">
status = #{status,jdbcType=VARCHAR},
</if>
</set>
where order_id = #{orderId,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="com.by.entity.TOrder">
update t_order
set
price = #{price,jdbcType=DECIMAL},
user_id = #{userId,jdbcType=BIGINT},
status = #{status,jdbcType=VARCHAR}
where order_id = #{orderId,jdbcType=BIGINT}
</update>
</mapper>
TDictMapper.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.by.dao.TDictDao">
<resultMap id="BaseResultMap" type="com.by.entity.TDict">
<id property="dictId" column="dict_id" jdbcType="BIGINT"/>
<result property="type" column="type" jdbcType="VARCHAR"/>
<result property="code" column="code" jdbcType="VARCHAR"/>
<result property="value" column="value" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
dict_id,type,code,
value
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_dict
where dict_id = #{dictId,jdbcType=BIGINT}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from t_dict
where dict_id = #{dictId,jdbcType=BIGINT}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TDict" useGeneratedKeys="true">
insert into t_dict
( dict_id,type,code
,value)
values (#{dictId,jdbcType=BIGINT},#{type,jdbcType=VARCHAR},#{code,jdbcType=VARCHAR}
,#{value,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TDict" useGeneratedKeys="true">
insert into t_dict
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="dictId != null">dictId,</if>
<if test="type != null">type,</if>
<if test="code != null">code,</if>
<if test="value != null">value,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="dictId != null">dict_id = #{dictId,jdbcType=BIGINT},</if>
<if test="type != null">type = #{type,jdbcType=VARCHAR},</if>
<if test="code != null">code = #{code,jdbcType=VARCHAR},</if>
<if test="value != null">value = #{value,jdbcType=VARCHAR},</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.by.entity.TDict">
update t_dict
<set>
<if test="type != null">
type = #{type,jdbcType=VARCHAR},
</if>
<if test="code != null">
code = #{code,jdbcType=VARCHAR},
</if>
<if test="value != null">
value = #{value,jdbcType=VARCHAR},
</if>
</set>
where dict_id = #{dictId,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="com.by.entity.TDict">
update t_dict
set
type = #{type,jdbcType=VARCHAR},
code = #{code,jdbcType=VARCHAR},
value = #{value,jdbcType=VARCHAR}
where dict_id = #{dictId,jdbcType=BIGINT}
</update>
</mapper>
TUserMapper.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.by.dao.TUserDao">
<resultMap id="BaseResultMap" type="com.by.entity.TUser">
<id property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="fullname" column="fullname" jdbcType="VARCHAR"/>
<result property="userType" column="user_type" jdbcType="CHAR"/>
</resultMap>
<sql id="Base_Column_List">
user_id,fullname,user_type
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user
where user_id = #{userId,jdbcType=BIGINT}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from t_user
where user_id = #{userId,jdbcType=BIGINT}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TUser" useGeneratedKeys="true">
insert into t_user
( user_id,fullname,user_type
)
values (#{userId,jdbcType=BIGINT},#{fullname,jdbcType=VARCHAR},#{userType,jdbcType=CHAR}
)
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TUser" useGeneratedKeys="true">
insert into t_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">userId,</if>
<if test="fullname != null">fullname,</if>
<if test="userType != null">userType,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id = #{userId,jdbcType=BIGINT},</if>
<if test="fullname != null">fullname = #{fullname,jdbcType=VARCHAR},</if>
<if test="userType != null">user_type = #{userType,jdbcType=CHAR},</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.by.entity.TUser">
update t_user
<set>
<if test="fullname != null">
fullname = #{fullname,jdbcType=VARCHAR},
</if>
<if test="userType != null">
user_type = #{userType,jdbcType=CHAR},
</if>
</set>
where user_id = #{userId,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="com.by.entity.TUser">
update t_user
set
fullname = #{fullname,jdbcType=VARCHAR},
user_type = #{userType,jdbcType=CHAR}
where user_id = #{userId,jdbcType=BIGINT}
</update>
</mapper>
TbLogMapper.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.by.dao.TbLogDao">
<resultMap id="BaseResultMap" type="com.by.entity.TbLog">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="info" column="info" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id,info
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from tb_log
where id = #{id,jdbcType=BIGINT}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from tb_log
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TbLog" useGeneratedKeys="true">
insert into tb_log
( id,info)
values (#{id,jdbcType=BIGINT},#{info,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.by.entity.TbLog" useGeneratedKeys="true">
insert into tb_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="info != null">info,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">id = #{id,jdbcType=BIGINT},</if>
<if test="info != null">info = #{info,jdbcType=VARCHAR},</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.by.entity.TbLog">
update tb_log
<set>
<if test="info != null">
info = #{info,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="com.by.entity.TbLog">
update tb_log
set
info = #{info,jdbcType=VARCHAR}
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>
上面的实体类和dao层,以及xml文件直接复制就行,重点不在这些,重点在配置文件
基于inline模式实现库内水平分表
SQL准备
新建库、表
order_db_1
├── t_order_1
└── t_order_2
#创建数据库
CREATE DATABASE `order_db_1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
#建表
USE order_db_1;
DROP TABLE IF EXISTS `t_order_1`;
CREATE TABLE `t_order_1` (
`order_id` BIGINT (20) NOT NULL COMMENT '订单id',
`price` DECIMAL (10, 2) NOT NULL COMMENT '订单价格',
`user_id` BIGINT (20) NOT NULL COMMENT '下单用户id',
`status` VARCHAR (50) CHARACTER
SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = INNODB CHARACTER
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `t_order_2`;
CREATE TABLE `t_order_2` (
`order_id` BIGINT (20) NOT NULL COMMENT '订单id',
`price` DECIMAL (10, 2) NOT NULL COMMENT '订单价格',
`user_id` BIGINT (20) NOT NULL COMMENT '下单用户id',
`status` VARCHAR (50) CHARACTER
SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = INNODB CHARACTER
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
配置文件
首先在application.properties文件指定Mybatis的Mapper文件
#下面内容是为了让mybatis映射
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*.xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=com.by.sharding.entity
因为我们要使用Sharding JDBC进行分库分表,所以不能用spring的数据源,接下来我们要配置Sharding JDBC
遵循以下配置数据源:
配置指定表的数据节点(也就是指定操作的逻辑表属于哪个库的哪个表)
指定库的分片策略
指定表的分片策略
因为我们接下来要根据不同的案例配置多个配置文件,为了防止配置文件太乱,直观一点,我采用的是多环境配置,这样只需要在application.properties
中激活指定的配置文件即可。
库内水平分表案例配置文件:application-test1.properties
#1.配置数据源
#数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.names= ds1
#数据库连接池类名称
spring.shardingsphere.datasource.ds1.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds1.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds1.url= jdbc:mysql://localhost:端口号/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds1.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds1.password= 数据库密码
#数据库连接池的其它属性
#spring.shardingsphere.datasource.ds1.xxx=
#2.配置指定表的数据节点(指定操作的逻辑表是属于哪个库的哪张表) t_order ----> t_order_1 、 t_order_2
#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。
# 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表
# (即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#<logic-table-name> 逻辑表的名字
#actual-data-nodes 真实数据节点
#此次没有跨库分表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes= ds1.t_order_${1..2}
#3.指定库的分片策略(说明:因为这里只指定一个库,没有进行水平分库处理,所以无需配置分库策略配置)
#行表达式分片策略
#分片列名称
#spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=
##分片算法行表达式,需符合groovy语法
#spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=
#4.指定表的分片策略(根据分片条件找到对应的真实表) 以order_id % 2 + 1来分表
#order_id 为片键
#分片列名称
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column= order_id
#分片算法行表达式,需符合groovy语法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression= t_order_${order_id % 2 + 1}
#打印SQL
#是否开启SQL显示,默认值: false
spring.shardingsphere.props.sql.show= true
注:注意多环境配置的语法结构
application-{profile}.properties
profile:代表的就是一套环境
在`application.properties`中激活指定的配置文件语法为:spring.profiles.active={profile}
测试类
package com.by;
import com.by.dao.TOrderDao;
import com.by.entity.TOrder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
import java.util.Random;
@SpringBootTest
class ShardingJdbcAppTest {
@Autowired
private TOrderDao tOrderDao;
/**
* 测试库内水平分表
*/
@Test
public void test1() {
int orderId = 0;
Random random = new Random();
for (int i = 0; i < 20; i++) {
//保证随机生成奇数或者偶数
orderId += random.nextInt(2) + 1;
TOrder order = TOrder.builder().orderId(Long.valueOf(orderId)).userId(Long.valueOf(i)).status("1").price(new BigDecimal(300)).build();
tOrderDao.insert(order);
}
}
}
基于inline模式实现水平分库分表
SQL准备
在order_db_1的基础上新建库、表
order_db_1
├── t_order_1
└── t_order_2
order_db_2
├── t_order_1
└── t_order_2
#继续构建order_db_2数据库
CREATE DATABASE `order_db_2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
#建表
USE order_db_2;
DROP TABLE IF EXISTS `t_order_1`;
CREATE TABLE `t_order_1` (
`order_id` BIGINT (20) NOT NULL COMMENT '订单id',
`price` DECIMAL (10, 2) NOT NULL COMMENT '订单价格',
`user_id` BIGINT (20) NOT NULL COMMENT '下单用户id',
`status` VARCHAR (50) CHARACTER
SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = INNODB CHARACTER
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `t_order_2`;
CREATE TABLE `t_order_2` (
`order_id` BIGINT (20) NOT NULL COMMENT '订单id',
`price` DECIMAL (10, 2) NOT NULL COMMENT '订单价格',
`user_id` BIGINT (20) NOT NULL COMMENT '下单用户id',
`status` VARCHAR (50) CHARACTER
SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = INNODB CHARACTER
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
配置文件
创建application-test2.properties
#1.配置数据源①
#数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.names= ds1,ds2
#数据库连接池类名称
spring.shardingsphere.datasource.ds1.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds1.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds1.url= jdbc:mysql://localhost:端口号/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds1.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds1.password= 数据库密码
#数据库连接池的其它属性
#spring.shardingsphere.datasource.ds1.xxx=
#1.配置数据源②
#数据库连接池类名称
spring.shardingsphere.datasource.ds2.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds2.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds2.url= jdbc:mysql://localhost:端口号/order_db_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds2.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds2.password= 数据库密码
#2.配置指定表的数据节点(指定操作的逻辑表是属于哪个库的哪张表) t_order ----> t_order_1 、 t_order_2
#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。
# 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表
# (即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#<logic-table-name> 逻辑表的名字
#actual-data-nodes 真实数据节点
#此次没有跨库分表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes= ds${1..2}.t_order_${1..2}
#3.指定库的分片策略(说明:因为这里只指定一个库,没有进行水平分库处理,所以无需配置分库策略配置)
#行表达式分片策略
#分片列名称(说明:找库仍然通过order_id? 换个思路,通过user_id)
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column= user_id
#分片算法行表达式,需符合groovy语法
#说明:此时分片表达式不可以写成t_order_${order_id % 2 + 1},
# 因为这样会造成数据倾斜,因为如果订单id为偶数,
# 那么所有偶数订单都会落在ds1.t_order_1,奇数订单落在ds2.t_order_2,
# 导致偶数订单和奇数订单的数据不均匀,从而导致数据倾斜。
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression= ds${user_id % 2 + 1}
#4.指定表的分片策略(根据分片条件找到对应的真实表) 以order_id % 2 + 1来分表
#order_id 为片键
#分片列名称
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column= order_id
#分片算法行表达式,需符合groovy语法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression= t_order_${order_id % 2 + 1}
#打印SQL
#是否开启SQL显示,默认值: false
spring.shardingsphere.props.sql.show= true
注:写的时候记得在application.properties更改要激活的配置环境
测试类
/**
* 测试水平分库分表
*/
@Test
public void test2() {
int orderId=0;
int userId=0;
Random random = new Random();
for (int i = 0; i < 40; i++) {
//保证随机生成奇数或者偶数
orderId+=random.nextInt(2)+1;
userId+=random.nextInt(2)+1;
TOrder order = TOrder.builder().orderId(Long.valueOf(orderId))
.userId(Long.valueOf(userId))
.status("1")
.price(new BigDecimal(300))
.build();
tOrderDao.insert(order);
}
}
sharding-JDBC广播表
广播表介绍
-
广播表属于数据库中数据量较小和变动较少,且存在高频联合查询的表,例如:数据字典表、状态表、类别表、配置表等属于广播表。
-
可以将这些类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。
SQL准备
广播表结构如下:
order_db_1
├── t_order_1
└── t_order_2
└── t_dict
order_db_2
├── t_order_1
└── t_order_2
└── t_dict
#在数据库 user_db、order_db_1、order_db_2中均要建表
CREATE TABLE `t_dict` (
`dict_id` BIGINT (20) NOT NULL COMMENT '字典id',
`type` VARCHAR (50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典类型',
`code` VARCHAR (50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典编码',
`value` VARCHAR (50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典值',
PRIMARY KEY (`dict_id`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
注:因为是广播表,所以需要在我们建的两个数据库内均要执行该sql
配置文件
广播表配置文件
复制application-test2.properties
文件并添加以下配置语句:
spring.shardingsphere.sharding.broadcast-tables= t_dict
Plain Text
完整配置如下:application-test3.properties
#1.配置数据源①
#数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.names= ds1,ds2
#数据库连接池类名称
spring.shardingsphere.datasource.ds1.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds1.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds1.url= jdbc:mysql://localhost:端口号/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds1.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds1.password= 数据库密码
#数据库连接池的其它属性
#spring.shardingsphere.datasource.ds1.xxx=
#1.配置数据源②
#数据库连接池类名称
spring.shardingsphere.datasource.ds2.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds2.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds2.url= jdbc:mysql://localhost:端口号/order_db_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds2.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds2.password= 数据库密码
#2.配置指定表的数据节点(指定操作的逻辑表是属于哪个库的哪张表) t_order ----> t_order_1 、 t_order_2
#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。
# 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表
# (即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#<logic-table-name> 逻辑表的名字
#actual-data-nodes 真实数据节点
#此次没有跨库分表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes= ds${1..2}.t_order_${1..2}
#3.指定库的分片策略(说明:因为这里只指定一个库,没有进行水平分库处理,所以无需配置分库策略配置)
#行表达式分片策略
#分片列名称(说明:找库仍然通过order_id? 换个思路,通过user_id)
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column= user_id
#分片算法行表达式,需符合groovy语法
#说明:此时分片表达式不可以写成t_order_${order_id % 2 + 1},
# 因为这样会造成数据倾斜,因为如果订单id为偶数,
# 那么所有偶数订单都会落在ds1.t_order_1,奇数订单落在ds2.t_order_2,
# 导致偶数订单和奇数订单的数据不均匀,从而导致数据倾斜。
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression= ds${user_id % 2 + 1}
#4.指定表的分片策略(根据分片条件找到对应的真实表) 以order_id % 2 + 1来分表
#order_id 为片键
#分片列名称
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column= order_id
#分片算法行表达式,需符合groovy语法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression= t_order_${order_id % 2 + 1}
#配置广播表(两种方式)
#①第一种:像数组形式配置
#spring.shardingsphere.sharding.broadcast-tables[0]= 第一个广播表名
#spring.shardingsphere.sharding.broadcast-tables[1]= 第二个广播表名
#spring.shardingsphere.sharding.broadcast-tables[X]= 第X个广播表名
#②第二种:像字符串形式配置,多个广播表以逗号分隔(说明这里我们只写了一个广播表,所以只写一个就行)
spring.shardingsphere.sharding.broadcast-tables= t_dict
#打印SQL
#是否开启SQL显示,默认值: false
spring.shardingsphere.props.sql.show= true
注:仍然不要忘记在在application.properties更改要激活的配置环境哦
测试类
@Autowired
private TDictMapper tDictMapper;
@Test
public void commonTable(){
TDict build = TDict.builder().dictId(1l).code("666").type("1").value("888")
.build();
tDictMapper.insert(build);
}
基于inline模式实现垂直分库
SQL准备
数据库结构如下:
order_db_1
├── t_order_1
└── t_order_2
└── t_dict
order_db_2
├── t_order_1
└── t_order_2
└── t_dict
user_db
└── t_user
#创建数据库
CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
#建表
USE user_db;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`user_id` BIGINT (20) NOT NULL COMMENT '用户id',
`fullname` VARCHAR (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名',
`user_type` CHAR (1) DEFAULT NULL COMMENT '用户类型',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = INNODB CHARACTER
SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
配置文件
垂直分库配置文件
复制application-test3.properties
文件,并添加添加以下配置语句:
# 配置数据源③
#数据库连接池类名称
spring.shardingsphere.datasource.udb.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.udb.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.udb.url= jdbc:mysql://localhost:端口号/user_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds2.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds2.password= 数据库密码
#针对t_user表属于一个库下的,没有进行分表,所以逻辑表和真实数据节点一致,也不需要配置任何分片策略
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes= udb.t_user
完整配置如下:application-test4.properties
#1.配置数据源①
#数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.names= ds1,ds2,udb,defdb
#数据库连接池类名称
spring.shardingsphere.datasource.ds1.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds1.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds1.url= jdbc:mysql://localhost:端口号/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds1.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds1.password= 数据库密码
#数据库连接池的其它属性
#spring.shardingsphere.datasource.ds1.xxx=
# 配置数据源②
#数据库连接池类名称
spring.shardingsphere.datasource.ds2.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds2.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds2.url= jdbc:mysql://localhost:端口号/order_db_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds2.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds2.password= 数据库密码
# 配置数据源③
#数据库连接池类名称
spring.shardingsphere.datasource.udb.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.udb.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.udb.url= jdbc:mysql://localhost:端口号/user_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.udb.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.udb.password= 数据库密码
# 配置数据源④
#数据库连接池类名称
spring.shardingsphere.datasource.defdb.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.defdb.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.defdb.url= jdbc:mysql://localhost:端口号/default_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.defdb.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.defdb.password= 数据库密码
#2.配置指定表的数据节点(指定操作的逻辑表是属于哪个库的哪张表) t_order ----> t_order_1 、 t_order_2
#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。
# 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表
# (即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#<logic-table-name> 逻辑表的名字
#actual-data-nodes 真实数据节点
#此次没有跨库分表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes= ds${1..2}.t_order_${1..2}
#针对t_user表属于一个库下的,没有进行分表,所以逻辑表和真实数据节点一致,也不需要配置任何分片策略
#----->针对垂直分库,我们的目的是专库专用,有多少个库就配置多少个数据源即可
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes= udb.t_user
#3.指定库的分片策略(说明:若只指定一个库,则不需要进行水平分库处理,所以无需配置分库策略配置)
#行表达式分片策略
#分片列名称(说明:找库仍然通过order_id? 换个思路,通过user_id)
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column= user_id
#分片算法行表达式,需符合groovy语法
#说明:此时分片表达式不可以写成t_order_${order_id % 2 + 1},
# 因为这样会造成数据倾斜,因为如果订单id为偶数,
# 那么所有偶数订单都会落在ds1.t_order_1,奇数订单落在ds2.t_order_2,
# 导致偶数订单和奇数订单的数据不均匀,从而导致数据倾斜。
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression= ds${user_id % 2 + 1}
#4.指定表的分片策略(根据分片条件找到对应的真实表) 以order_id % 2 + 1来分表
#order_id 为片键
#分片列名称
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column= order_id
#分片算法行表达式,需符合groovy语法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression= t_order_${order_id % 2 + 1}
#配置广播表(两种方式)
#①第一种:像数组形式配置
#spring.shardingsphere.sharding.broadcast-tables[0]= 第一个广播表名
#spring.shardingsphere.sharding.broadcast-tables[1]= 第二个广播表名
#spring.shardingsphere.sharding.broadcast-tables[X]= 第X个广播表名
#②第二种:像字符串形式配置,多个广播表以逗号分隔(说明这里我们只写了一个广播表,所以只写一个就行)
spring.shardingsphere.sharding.broadcast-tables= t_dict
#打印SQL
#是否开启SQL显示,默认值: false
spring.shardingsphere.props.sql.show= true
测试类
/**
* 测试垂直分库
*/
@Test
public void test04() {
TUser user = TUser.builder().userId(133l).userType("1")
.fullname("laozhang").build();
tUserDao.insert(user);
}
默认数据源
什么是默认数据源呢?我们可以理解为如果没有配置分片策略,则可指定默认访问的数据源,也就是说只需要指定数据源,无需指定数据节点、库和表的分片策略也无需执行。
如果在相关的操作时,发现逻辑表没有对应的数据节点、库表的分片配置,那么就走指定的数据源;
SQL准备
-- 构建数据
create database default_db character set utf8;
use default_db;
-- 构建表
create table tb_log (
id bigint primary key ,
info varchar(30)
);
配置文件
直接在application-test4.properties
文件中添加以下配置
#数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.names= ds1,ds2,udb,defdb
# 配置数据源④
#数据库连接池类名称
spring.shardingsphere.datasource.defdb.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.defdb.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.defdb.url= jdbc:mysql://localhost:端口号/default_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.defdb.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.defdb.password= 数据库密码
#配置默认数据源(当大量的表存在于单个库中,并且库内没有水平分表处理,可使用默认数据源)
#未配置分片规则的表将通过默认数据源定位
spring.shardingsphere.sharding.default-data-source-name=defdb
测试类
/**
* 测试默认数据源
* 对于没有做分片处理的操作,则会直接访问默认数据源处理
*/
@Test
public void test5() {
TbLog log = TbLog.builder().id(1l).info("这是一个测试").build();
tbLogDao.insert(log);
}
inline模式小结
-
优点:
-
配置简单,开发成本低;
-
便于维护和理解;
-
缺点:
-
复杂的分片策略支持不友好;
-
不支持范围查询
Standard模式实现分库分表
官方API介绍
关于阅读官方API,我们可以发现基于标准方式实现分库分表,需要分别为库和表定义精准查询 和范围查询实现:
PreciseShardingAlgorithm :精准查询库或者表(sql使用使用了= 、in)
RangeShardingAlgorithm :范围查询库或者表 (between and 、 > 、 <)
所以,使用sharding-jdbc时尽量让分片键去查询,且遵循使用规范;
分库策略
分别实现PreciseShardingAlgorithm和RangeShardingAlgorithm 接口并实现doSharding方法
package com.by.alg;
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
import java.util.Optional;
public class Common4Db implements PreciseShardingAlgorithm<Long> , RangeShardingAlgorithm<Long> {
//接口后面的Long类型,代表分片键的类型,我们进行分类库的分片键是user_id,所以是Long类型
/**
* 精准查询实现方法
* @param dbNames 数据源名称集合,该集合数据源在配置文件中配置,如ds1,ds2,从这两个中选择一个返回
* @param ShardingValue 封装了逻辑表、分片列名称、条件值等
* @return
*/
@Override
public String doSharding(Collection<String> dbNames, PreciseShardingValue<Long> ShardingValue) {
//inline模式下的分片算法:ds${user_id % 2 + 1}
//获取逻辑表名称 t_order
String logicTableName = ShardingValue.getLogicTableName();
//获取数据库指定的分片键名称 从配置文件中获取 user_id
String columnName = ShardingValue.getColumnName();
//获取分片查询的条件值
Long value = ShardingValue.getValue();
//自定义算法:根据分片键值,获取数据库名称
//过滤出来的数据源名称集合中,以分片键值作为后缀的,返回
//Optional集合是jdk1.8的新特性,代表一个值存在或不存在,可以避免空指针异常
Optional<String> result = dbNames.stream().filter(dbName -> dbName.endsWith(value % 2 + 1 + "")).findFirst();
//判断集合中是否有元素
if(result.isPresent())
{
return result.get();
}
return null;
}
/**
* 范围查询实现方法
* @param dbNames 数据源名称集合,该集合数据源在配置文件中配置,如ds1,ds2
* @param ShardingValue 封装了逻辑表、分片列名称、条件值等
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> dbNames, RangeShardingValue<Long> ShardingValue) {
//获取逻辑表名称 t_order
String logicTableName = ShardingValue.getLogicTableName();
//获取数据库指定的分片键名称 从配置文件中获取 user_id
String columnName = ShardingValue.getColumnName();
//获取范围值
Range<Long> valueRange = ShardingValue.getValueRange();
//理论上应该让范围值参与运算,然后根据某种算法规则获取数据源名称
//hasLowerBound(),hasUpperBound() 是否存在下限和上限。返回布尔值
if (valueRange.hasLowerBound() && valueRange.hasUpperBound()) {
//获取下限值
Long lower = valueRange.lowerEndpoint();
//获取上限值
Long upper = valueRange.upperEndpoint();
//自定义算法:根据分片键值,获取数据库名称
//过滤出来的数据源名称集合中,以分片键值作为后缀的,返回
}
return dbNames;
}
}
分表策略
分别实现PreciseShardingAlgorithm和RangeShardingAlgorithm 接口并实现doSharding方法
package com.by.alg;
import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
import java.util.Optional;
public class Common4Tb implements PreciseShardingAlgorithm<Long> , RangeShardingAlgorithm<Long> {
//接口后面的Long类型,代表分片键的类型,我们进行分类库的分片键是user_id,所以是Long类型
/**
* 精准查询实现方法
* @param tbNames 数据节点名称集合,该集合数据源在配置文件中配置,如 t_order_1,t_order_2,从这两个中选择一个返回
* @param ShardingValue 封装了逻辑表、分片列名称、条件值等
* @return
*/
@Override
public String doSharding(Collection<String> tbNames, PreciseShardingValue<Long> ShardingValue) {
//inline模式下的分片算法:ds${user_id % 2 + 1}
//获取逻辑表名称 t_order
String logicTableName = ShardingValue.getLogicTableName();
//获取数据库指定的分片键名称 从配置文件中获取 user_id
String columnName = ShardingValue.getColumnName();
//获取分片查询的条件值
Long value = ShardingValue.getValue();
//自定义算法:根据分片键值,获取数据库名称
//过滤出来的数据源名称集合中,以分片键值作为后缀的,返回
//Optional集合是jdk1.8的新特性,代表一个值存在或不存在,可以避免空指针异常
Optional<String> result = tbNames.stream().filter(tbName -> tbName.endsWith(value % 2 + 1 + "")).findFirst();
//判断集合中是否有元素
if(result.isPresent())
{
return result.get();
}
return null;
}
/**
* 范围查询实现方法
* @param tbNames 数据节点名称集合,该集合数据源在配置文件中配置,如 t_order_1,t_order_2
* @param ShardingValue 封装了逻辑表、分片列名称、条件值等
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> tbNames, RangeShardingValue<Long> ShardingValue) {
//获取逻辑表名称 t_order
String logicTableName = ShardingValue.getLogicTableName();
//获取数据库指定的分片键名称 从配置文件中获取 user_id
String columnName = ShardingValue.getColumnName();
//获取范围值
Range<Long> valueRange = ShardingValue.getValueRange();
//理论上应该让范围值参与运算,然后根据某种算法规则获取数据源名称
//hasLowerBound(),hasUpperBound() 是否存在下限和上限。返回布尔值
if (valueRange.hasLowerBound() && valueRange.hasUpperBound()) {
//获取下限值
Long lower = valueRange.lowerEndpoint();
//获取上限值
Long upper = valueRange.upperEndpoint();
//自定义算法:根据分片键值,获取数据库名称
//过滤出来的数据源名称集合中,以分片键值作为后缀的,返回
}
return tbNames;
}
}
配置类
#1.配置数据源①
#数据源名称,多数据源以逗号分隔
spring.shardingsphere.datasource.names= ds1,ds2,udb,defdb
#数据库连接池类名称
spring.shardingsphere.datasource.ds1.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds1.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds1.url= jdbc:mysql://localhost:端口号/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds1.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds1.password= 数据库密码
#数据库连接池的其它属性
#spring.shardingsphere.datasource.ds1.xxx=
# 配置数据源②
#数据库连接池类名称
spring.shardingsphere.datasource.ds2.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.ds2.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.ds2.url= jdbc:mysql://localhost:端口号/order_db_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.ds2.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.ds2.password= 数据库密码
# 配置数据源③
#数据库连接池类名称
spring.shardingsphere.datasource.udb.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.udb.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.udb.url= jdbc:mysql://localhost:端口号/user_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.udb.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.udb.password= 数据库密码
# 配置数据源④
#数据库连接池类名称
spring.shardingsphere.datasource.defdb.type= com.alibaba.druid.pool.DruidDataSource
#数据库驱动类名
spring.shardingsphere.datasource.defdb.driver-class-name= com.mysql.cj.jdbc.Driver
#数据库url连接
spring.shardingsphere.datasource.defdb.url= jdbc:mysql://localhost:端口号/default_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
spring.shardingsphere.datasource.defdb.username= 数据库用户名
#数据库密码
spring.shardingsphere.datasource.defdb.password= 数据库密码
#2.配置指定表的数据节点(指定操作的逻辑表是属于哪个库的哪张表) t_order ----> t_order_1 、 t_order_2
#由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。
# 缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表
# (即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
#<logic-table-name> 逻辑表的名字
#actual-data-nodes 真实数据节点
#此次没有跨库分表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes= ds${1..2}.t_order_${1..2}
#针对t_user表属于一个库下的,没有进行分表,所以逻辑表和真实数据节点一致,也不需要配置分片策略
#----->针对垂直分库,我们的目的是专库专用,有多少个库就配置多少个数据源即可
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes= udb.t_user
#3.指定库的分片策略(说明:因为这里只指定一个库,没有进行水平分库处理,所以无需配置分库策略配置)
#行表达式分片策略
#分片列名称(说明:找库仍然通过order_id? 换个思路,通过user_id)
#spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column= user_id
#分片算法行表达式,需符合groovy语法
#说明:此时分片表达式不可以写成t_order_${order_id % 2 + 1},
# 因为这样会造成数据倾斜,因为如果订单id为偶数,
# 那么所有偶数订单都会落在ds1.t_order_1,奇数订单落在ds2.t_order_2,
# 导致偶数订单和奇数订单的数据不均匀,从而导致数据倾斜。
#spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression= ds${user_id % 2 + 1}
#配置精准查询和范围查询的类
common.shard4db= com.by.alg.Common4Db
common.shard4tb= com.by.alg.Common4Tb
#用于单分片键的标准分片场景
#分片列名称
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column= user_id
#精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name= ${common.shard4db}
#范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.range-algorithm-class-name= ${common.shard4db}
#4.指定表的分片策略(根据分片条件找到对应的真实表) 以order_id % 2 + 1来分表
#order_id 为片键
#分片列名称
#spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column= order_id
#分片算法行表达式,需符合groovy语法
#spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression= t_order_${order_id % 2 + 1}
#分片列名称
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column= order_id
#精确分片算法类名称,用于=和IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name= ${common.shard4tb}
#范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.range-algorithm-class-name= ${common.shard4tb}
#配置广播表(两种方式)
#①第一种:像数组形式配置
#spring.shardingsphere.sharding.broadcast-tables[0]= 第一个广播表名
#spring.shardingsphere.sharding.broadcast-tables[1]= 第二个广播表名
#spring.shardingsphere.sharding.broadcast-tables[X]= 第X个广播表名
#②第二种:像字符串形式配置,多个广播表以逗号分隔(说明这里我们只写了一个广播表,所以只写一个就行)
spring.shardingsphere.sharding.broadcast-tables= t_dict
#配置默认数据源(当大量的表存在于单个库中,并且库内没有水平分表处理,可使用默认数据源)
#未配置分片规则的表将通过默认数据源定位
spring.shardingsphere.sharding.default-data-source-name=defdb
#打印SQL
#是否开启SQL显示,默认值: false
spring.shardingsphere.props.sql.show= true
测试类
/**
* @Description 测试数据库标准精准查询=
*/
@Test
public void test7(){
List<TOrder> tOrder = tOrderDao.findByUserId(18l);
System.out.println(tOrder);
}
/**
* @Description 测试标准查询:范围匹配 between
*/
@Test
public void test8(){
List<TOrder> tOrder = tOrderDao.selectByRange(18l,70l);
System.out.println(tOrder);
}
/**
* @Description 测试表的精准查询
* 查询中没有使用user_id,则不会走自定义的精准和范围查询,而是直接全库查询
* 但是查询时根据order_id等值查询,所以会走表的精准查询逻辑
*/
@Test
public void testPrecisTable(){
TOrder tOrder = tOrderDao.selectByPrimaryKey(34l);
System.out.println(tOrder);
}
/**
* @Description 测试精准匹配表的范围查询
*/
@Test
public void testRangeTable(){
List<TOrder> tOrders = tOrderDao.selectByRange(1l, 40l);
System.out.println(tOrders);
}
/**
* @Description 整体标准擦护心
*/
@Test
public void testAll(){
//结论:因为user_id是等值查询,所以对于数据库调用精准查询算法类
//因为order_id使用between ,那么会被调用表的范围查询的算法类
List<TOrder> tOrders = tOrderDao.selectByRange2(36l, 10l, 60l);
System.out.println(tOrders);
}