简介
关于shardingsphere的基本相关概念的学习和使用shardingjdbc进行水平分表的例子
shardingsphere
是一套开源的分布式关系型数据库中间件解决方案,由sharding-jdbc,sharding-proxy,sharding-sidecar三个项目组成。
定位为关系型数据库中间件,合理在分布式环境下使用关系型数据库操作。
分库分表
随着时间和业务的发展,表中的数据会越来越多,这时,对数据库表进行crud操作的时候,会有性能问题。
解决方案:
- 从硬件上
- 分库分表
分库分表,主要分为:垂直分库,水平分库,垂直分表,水平分表
垂直分表
将一张表按照字段分成多个表,每个表存储其中的一部分字段。
一般按照字段的访问频率高低来区分
- 将热门字段,冷门字段分别存到不同的表中
- 把text,blob等大字段拆分出来放到附表中
- 经常组合查询的列放到一张表中
好处:提高热门数据的操作效率上,避免io的争抢。
垂直分库
按照业务将表进行分类,存储到不同的数据库中,每个库可以放到不同的服务器上。
好处:
- 降低业务层面的耦合,使得业务更清晰
- 方便对数据进行分级管理,维护,监控和扩展等
- 高并发场景下,可以提升io,数据库连接数,降低单机硬件资源的瓶颈
水平水库
把一个表的数据按照一定的规则拆分到不同的数据库中,每个库可以放到不同的服务器上。拆分出来的多张表结构是一样的。
好处:
- 解决了单库大数据,高并发的性能瓶颈
- 提高了系统的稳定性和可用性。一个库出问题,还有部分可用。
- io冲突减少,锁定减少
水平分表
如果水平分库太多,运维成本会增加,就可以水平分表。
水平分表:把一个表的数据按一定规则拆到多个表中。
好处:
- 解决单一表数据量过大而导致的性能问题
- 避免io争抢并减少锁表的概率
最佳实践
系统设计阶段就根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量不是很大的时候,首先考虑缓存,读写分离,索引等方案。当数据量很大而且持续增长的时候,考虑水平分库水平分表方案。
分库分表问题
- 跨节点连接查询问题(分页,排序)
- 多数据源管理问题
shardingjdbc
开源的分布式数据库中间件,是轻量级的Java框架,在jdbc层提供额外的服务,使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的jdbc驱动,完全兼容jdbc和各种orm框架。
核心功能
- 数据分片
- 读写分离
shardingjdbc做的不是分库分表,而是让我们更简单地去操作分库分表里的数据。
通过shardingjdbc可以透明的访问已经分库分表,读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
程序员不用知道多少张表,多少个数据源,通过shardingjdbc就像操作单个数据源单个表一样去操作。
性能
对于单库单表来说,shardingjdbc的性能比jdbc低一点点,不超7%。
对于多库多表来说,shardingjdbc的性能比jdbc高2倍左右。
水平分表例子
在一个数据库中,有两张同样结构的表,插入数据的时候,偶数插入course_1表,奇数插入course_2表。
创建数据库和表
创建数据库
创建表一course_1
CREATE TABLE `course_1` (
`cid` bigint NOT NULL,
`cname` varchar(50) NOT NULL,
`user_id` bigint NOT NULL,
`cstatus` varchar(10) NOT NULL,
PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
创建表二course_2
CREATE TABLE `course_2` (
`cid` bigint NOT NULL,
`cname` varchar(50) NOT NULL,
`user_id` bigint NOT NULL,
`cstatus` varchar(10) NOT NULL,
PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
创建springboot项目
pom.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>shardingjdbcdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shardingjdbcdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
实体类course
package com.christ.entity;
import lombok.Data;
@Data
public class course {
private Long cid;
private String cname;
private Long userId;
private String cstatus;
}
mapper接口CourseMapper
package com.christ.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.christ.entity.course;
import org.springframework.stereotype.Repository;
@Repository
public interface CourseMapper extends BaseMapper<course> {
}
启动类DemoApplication
package com.christ;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.christ.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
配置文件application.properties
# 配置数据源,给数据源起名称
spring.shardingsphere.datasource.names=m1
#让后加载的覆盖先加载的。
spring.main.allow-bean-definition-overriding=true
#配置数据源具体内容,包含连接池,驱动,地址,用户名和密码
spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/course_db?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123456
#指定course表分布情况,配置表在哪个数据库里面,表名称都是什么 m1.course_1 , m1.course_2
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m1.course_$->{1..2}
# 指定course表里面主键cid 生成策略 SNOWFLAKE(雪花算法)
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
# 指定分片策略 约定cid值偶数添加到course_1表,如果cid是奇数添加到course_2表
spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid % 2 + 1}
# 打开sql输出日志
spring.shardingsphere.props.sql.show=true
解析:
course 是实际的数据库表名。
m1.course_$->{1…2} 是该表在数据库中的实际数据节点,也就是物理分片。
这种配置是用来描述数据库分片的策略,即将逻辑表 course 在物理上分散存储在不同的数据库节点中,每个节点对应一个具体的物理表。
所以,course 表不是虚拟表,而是一个真实存在的逻辑表,只是它的数据在物理上被分片存储在多个数据库节点中。
测试类
package com.christ;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.christ.entity.course;
import com.christ.mapper.CourseMapper;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
class DemoApplicationTests {
@Resource
private CourseMapper courseMapper;
@Test
public void add() {
course c = new course();
c.setCname("christ");
c.setUserId(1L);
c.setCstatus("good");
courseMapper.insert(c);
}
@Test
public void select(){
QueryWrapper<course> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("cid",1024200364422856705L);
course c = courseMapper.selectOne(queryWrapper);
System.out.println(c);
}
}
查看数据库
水平分库例子
有两个数据库edu_db_1,edu_db_2,两个数据库中都分表有course_1和course_2表。插入数据时,当userid为偶数时添加到edu_db_1中,当userid为奇数时添加到edu_db_2中;当cid为偶数时,添加到course_1表,当cid为奇数时,添加到course_2表。
在原来的springboot项目上修改
创建数据库和表
sql语句:
CREATE TABLE `course_1` (
`cid` bigint NOT NULL,
`cname` varchar(50) NOT NULL,
`user_id` bigint NOT NULL,
`cstatus` varchar(10) NOT NULL,
PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
写配置文件
# shardingjdbc分片策略
# 配置数据源,给数据源起名称,
# 水平分库,配置两个数据源
spring.shardingsphere.datasource.names=m1,m2
#让后加载的覆盖先加载的。
spring.main.allow-bean-definition-overriding=true
#配置第一个数据源具体内容,包含连接池,驱动,地址,用户名和密码
spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/edu_db_1?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123456
#配置第二个数据源具体内容,包含连接池,驱动,地址,用户名和密码
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3306/edu_db_2?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=1234556
#指定数据库分布情况,数据库里面表分布情况
# m1 m2 course_1 course_2
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m$->{1..2}.course_$->{1..2}
# 指定course表里面主键cid 生成策略 SNOWFLAKE
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
# 指定表分片策略 约定cid值偶数添加到course_1表,如果cid是奇数添加到course_2表
spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid % 2 + 1}
# 指定数据库分片策略 约定user_id是偶数添加m1,是奇数添加m2
spring.shardingsphere.sharding.tables.course.database-strategy.inline..sharding-column=user_id
spring.shardingsphere.sharding.tables.course.database-strategy.inline.algorithm-expression=m$->{user_id % 2 + 1}
# 打开sql输出日志
spring.shardingsphere.props.sql.show=true
测试类
package com.christ;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.christ.entity.course;
import com.christ.mapper.CourseMapper;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
class DemoApplicationTests {
@Resource
private CourseMapper courseMapper;
//水平分库
@Test
public void add1() {
course c = new course();
c.setCname("mike");
c.setUserId(3L);
c.setCstatus("good");
courseMapper.insert(c);
}
@Test
public void select(){
QueryWrapper<course> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("cid",1024380349527883777l);
course c = courseMapper.selectOne(queryWrapper);
System.out.println(c);
}
}
add的结果
垂直分库
专库专用,插入,查询用户信息的时候会操作user_db数据库中的t_user表。
添加数据库
创建user_db数据库,里面创建t_user表
CREATE TABLE `t_user` (
`user_id` bigint NOT NULL,
`username` varchar(100) NOT NULL,
`ustatus` varchar(50) NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
实体类user
package com.christ.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName(value = "t_user")
public class user {
private Long userId;
private String username;
private String ustatus;
}
mapper接口UserMapper
package com.christ.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.christ.entity.user;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<user> {
}
配置文件application.properties
# 配置数据源,给数据源起名称,
spring.shardingsphere.datasource.names=m1,m2,m0
# 一个实体类对应两张表,覆盖
spring.main.allow-bean-definition-overriding=true
#配置第一个数据源具体内容,包含连接池,驱动,地址,用户名和密码
spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/edu_db_1?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123456
#配置第二个数据源具体内容,包含连接池,驱动,地址,用户名和密码
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3306/edu_db_2?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=123456
#配置第三个数据源具体内容,包含连接池,驱动,地址,用户名和密码
spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3306/user_db?serverTimezone=GMT%2B8
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=123456
# 配置user_db数据库里面t_user 专库专表
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes=m$->{0}.t_user
# 指定course表里面主键cid 生成策略 SNOWFLAKE
spring.shardingsphere.sharding.tables.t_user.key-generator.column=user_id
spring.shardingsphere.sharding.tables.t_user.key-generator.type=SNOWFLAKE
# 指定表分片策略 约定cid值偶数添加到course_1表,如果cid是奇数添加到course_2表
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.algorithm-expression=t_user
#指定数据库分布情况,数据库里面表分布情况
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m$->{1..2}.course_$->{1..2}
# 指定course表里面主键cid 生成策略 SNOWFLAKE
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
# 指定表分片策略 约定cid值偶数添加到course_1表,如果cid是奇数添加到course_2表
spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid % 2 + 1}
# 指定数据库分片策略 约定user_id是偶数添加m1,是奇数添加m2
spring.shardingsphere.sharding.tables.course.database-strategy.inline..sharding-column=user_id
spring.shardingsphere.sharding.tables.course.database-strategy.inline.algorithm-expression=m$->{user_id % 2 + 1}
# 打开sql输出日志
spring.shardingsphere.props.sql.show=true
测试类DemoApplicationTests
package com.christ;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.christ.entity.user;
import com.christ.mapper.CourseMapper;
import com.christ.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
class DemoApplicationTests {
@Resource
private CourseMapper courseMapper;
@Resource
private UserMapper userMapper;
//垂直分库
@Test
public void add(){
user u = new user();
u.setUsername("john");
u.setUstatus("good");
userMapper.insert(u);
}
@Test
public void find(){
QueryWrapper<user> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id",1024403036157509633l);
user user1 = userMapper.selectOne(queryWrapper);
System.out.println(user1);
}
}
运行结果
公共表
用于存储一些变化很少,查询时候需要经常关联的数据的表,类似于数据字典,需要在每个数据库中都创建出相同结构公共表。
公共表例子
添加表
在edu_1,edu_2,user_db三个数据库中都创建t_udict表。
CREATE TABLE `t_udict` (
`dictid` bigint NOT NULL,
`ustatus` varchar(100) NOT NULL,
`uvalue` varchar(100) NOT NULL,
PRIMARY KEY (`dictid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
修改配置文件
在配置文件application.properties中,添加上:
# 配置公共表
spring.shardingsphere.sharding.broadcast-tables=t_udict
spring.shardingsphere.sharding.tables.t_udict.key-generator.column=dictid
spring.shardingsphere.sharding.tables.t_udict.key-generator.type=SNOWFLAKE
编写实体类
@Data
@TableName(value = "t_udict")
public class udict {
private Long dictid;
private String ustatus;
private String uvalue;
}
编写mapper接口
@Repository
public interface UdictMapper extends BaseMapper<udict> {
}
测试
package com.christ;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.christ.entity.udict;
import com.christ.entity.user;
import com.christ.mapper.CourseMapper;
import com.christ.mapper.UdictMapper;
import com.christ.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
class DemoApplicationTests {
@Resource
private CourseMapper courseMapper;
@Resource
private UserMapper userMapper;
@Resource
private UdictMapper udictMapper;
//公共表
@Test
public void add(){
udict u = new udict();
u.setUstatus("1");
u.setUvalue("good");
udictMapper.insert(u);
}
@Test
public void delete(){
QueryWrapper<udict> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("dictid",1024461354494328833l);
udictMapper.delete(queryWrapper);
}
}
运行结果: