zoukankan      html  css  js  c++  java
  • 分库分表中间件sharding-jdbc的使用

    数据分片产生的背景,可以查看https://shardingsphere.apache.org/document/current/cn/features/sharding/,包括了垂直拆分和水平拆分的概念.还有这个框架的目标是什么,都写得很清楚

    Sharding-JDBC与MyCat:

    • 解决分库分表的中间件.
    • 但是定位不同,Sharding-JDBC定位是轻量级Java框架,以jar包的方式提供服务,未使用中间件,使用代码连接库.MyCat相当于代理,MyCat相当于数据库,直接访问MyCat就可以,不需要关系库和表,MyCat自动处理,但是需要维护MyCat,性能会有损耗.

    Sharding-JDBC(1.x):

    github地址: https://github.com/apache/incubator-shardingsphere/releases
    官网: https://shardingsphere.incubator.apache.org/
    文档: https://shardingsphere.apache.org/document/current/en/overview/

    功能:

    • 分库分表:
      • SQL解析功能完善,支持聚合,分组,排序,LIMIT,OR等查询,并且支持级联表以及笛卡尔积的表查询
      • 支持内、外连接查询
      • 分片策略灵活,可支持=,BETWEEN,IN等多维度分片,也可支持多分片键共用,以及自定义分片策略
      • 基于Hint的强制分库分表路由
    • 读写分离:
      • 一主多从的读写分离配置,可配合分库分表使用
      • 基于Hint的强制主库路由
    • 分布式事务:
      • 最大努力送达型事务
      • TCC型事务(TBD)
    • 兼容性: 兼容各大ORM框架
      • 可适用于任何基于java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC
      • 可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等
      • 理论上可支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL
    • 灵活多样配置:
      • Java
      • Spring命名空间
      • YAML
      • Inline表达式
    • 分布式生成全局主键: 统一的分布式基于时间序列的ID生成器

    使用Sharding-JDBC进行读写分离实战

    在数据库的操作中,写操作是非常耗时的,而最常用的是读操作,读写分离的目的是避免数据库的写操作影响读操作的效率.最重要的目的还是减少数据库的压力,提高性能.

    这只是模仿读写分析实战,流程是创建两个数据库,配置两个数据源,一个是主表,一个是从表,写修改删除在主表,查询是在从表.

    1. 创建数据库的语句:
    // 主表
    CREATE DATABASE `ds_0` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
    // 从表
    CREATE DATABASE `ds_1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
    
    // 两个库中都创建表
    CREATE TABLE `user`(
    	id bigint(64) not null auto_increment,
    	city varchar(20) not null,
    	name varchar(20) not null,
    	PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    // 插入ds_0
    insert into user values(1001,'上海','尹吉欢');
    // 插入ds_1
    insert into user values(1002,'北京','张三');
    
    1. 创建项目,引入依赖
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>com.dangdang</groupId>
        <artifactId>sharding-jdbc-config-spring</artifactId>
        <version>1.5.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    1. 配置文件的编写(使用xml的方式来实现):
    // 创建sharding.xml,内容如下
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:rdb="http://www.dangdang.com/schema/ddframe/rdb" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/context 
                            http://www.springframework.org/schema/context/spring-context.xsd 
                            http://www.dangdang.com/schema/ddframe/rdb 
                            http://www.dangdang.com/schema/ddframe/rdb/rdb.xsd 
                            ">
        
        <!-- 主数据 -->
        <bean id="ds_0" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" primary="true">
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/ds_0?serverTimezone=UTC&amp;characterEncoding=utf-8&amp;useInformationSchema=true" />
            <property name="username" value="root" />
            <property name="password" value="nrblwbb7" />
        </bean>
        
        <!-- 从数据 -->
        <bean id="ds_1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/ds_1?serverTimezone=UTC&amp;ccharacterEncoding=utf-8" />
            <property name="username" value="root" />
            <property name="password" value="nrblwbb7" />
        </bean>
        
        <!-- 读写分离数据源 -->
        <rdb:master-slave-data-source id="dataSource" master-data-source-ref="ds_0" slave-data-sources-ref="ds_1"/>
         
        <!-- 增强版JdbcTemplate -->
        <!--<bean id="cxytiandiJdbcTemplate" class="com.cxytiandi.jdbc.CxytiandiJdbcTemplate">
        	<property name="dataSource" ref="dataSource"/>
        	<constructor-arg>
        		<value>com.cxytiandi.shardingjdbc.po</value>
        	</constructor-arg>
        </bean>
        -->
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    </beans>
    
    1. 编写model,service,controller
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public class User implements Serializable {
    
        private static final long serialVersionUID = -1205226416664488559L;
    
        private Long id;
    
        private String city = "";
    
        private String name = "";
    
    
    }
    
    
    public interface UserService {
    
        void save(User user);
    
    
        Object findAll();
    }
    
    
    @Service
    @Slf4j
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public void save(User user) {
            jdbcTemplate.execute("INSERT INTO USER(city,name) values ('"+user.getCity()+"','"+user.getName()+"')");
            log.info("进行插入操作, {} : ","插入成功");
        }
    
    
        @Override
        public Object findAll() {
            Integer integer = jdbcTemplate.queryForObject("SELECT COUNT(id) FROM USER", Integer.class);
            log.info("从表的数据的条数是 {} 条",integer);
            return integer;
        }
    }
    
    
    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @GetMapping("/save")
        public String save(){
            userService.save(User.builder().id(1001L).city("运城").name("王智").build());
            return "OK";
        }
    
        @GetMapping("/list")
        public Object list(){
            return userService.findAll();
        }
    }
    
    1. 启动类:
    @SpringBootApplication
    @Slf4j
    @ImportResource(locations = {"classpath:sharding.xml"})
    public class ShardingJdbcDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ShardingJdbcDemoApplication.class, args);
        }
    
    }
    

    运行进行访问,先进行save操作,到数据库查看可以看到两条数据,之后进行list操作,返回结果1,说明插入(写)操作在主表,查询在从表.

    强制路由

    以为在主表和从表之间同步是需要时间的,所以有的时候在写完之后就要立即进行读操作,所以这个时候就需要强制路由,让从主表中读取.

    ShardingSphere使用ThreadLocal管理分片键值。可以通过编程的方式向HintManager中添加分片条件,该分片条件仅在当前线程内生效。

     HintManager.getInstance().setMasterRouteOnly();
    

    在查询前使用这句可以指定从主库中进行读取数据.

    分片算法

    参考: https://blog.csdn.net/gjx8010/article/details/72542207#1-分片枚举

    1. 分片枚举: 通过在配置文件中配置可能的枚举id,自己配置分片。 这种规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,而全国的省份区县固定的,这类业务使用这一规则。
    2. 范围约定: 此分片适用于提前规划好分片字段某个范围属于哪个分片. 这个接触过,就是比如说id在110000的在一张表,1000120000在另一张表.
    3. 取模: 比如说两张表,奇数存一张表,偶数存一张表.
    4. 按日期进行分片: 比如说一天一张表,或者一个月一张表(这个一般是看业务需求).

    还有很多,不过我觉得我比较喜欢的就这几个,Hash的也很常用,只是我没有用过.真正用过的就是范围约定了.

    分库分表

    分库分表就是表面上的意思,将一个库分为多个库,讲一个表分为多个表.

    单库分表

    在前一个项目上进行修改

    1. 首先创建数据库ds_03,在数据库中创建4张表
    CREATE DATABASE `ds_2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
    
    CREATE TABLE `user_0` (
      `id` bigint(64) NOT NULL AUTO_INCREMENT,
      `city` varchar(20) NOT NULL,
      `name` varchar(20) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;
    
    依次创建user_1,user_2,user_3
    
    1. 重新创建xml文件sharding-table.xml:
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:rdb="http://www.dangdang.com/schema/ddframe/rdb" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/context 
                            http://www.springframework.org/schema/context/spring-context.xsd 
                            http://www.dangdang.com/schema/ddframe/rdb 
                            http://www.dangdang.com/schema/ddframe/rdb/rdb.xsd 
                            ">
       <!-- inline表达式报错,就是下面user_${id.longValue() % 4}} -->
       <context:property-placeholder  ignore-unresolvable="true"/> 
                           
        <!-- 主数据 -->
        <bean id="ds_2" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" primary="true">
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/ds_2?serverTimezone=UTC&amp;characterEncoding=utf-8" />
            <property name="username" value="root" />
            <property name="password" value="nrblwbb7" />
        </bean>
        
    
        <!-- algorithm-class="com.cxytiandi.shardingjdbc.UserSingleKeyTableShardingAlgorithm" -->
        <!-- user_0,user_1,user_2,user_3 -->
        <!-- 根据用户id来进行分表,使用inline表达式 -->
        <rdb:strategy id="userTableStrategy" sharding-columns="id" algorithm-expression="user_${id.longValue() % 4}"/>
        <!--使用自定义表达式-->
        <!--<rdb:strategy id="userTableStrategy" sharding-columns="id" algorithm-class="com.sharding.shardingjdbcdemo.UserSingleKeyTableShardingAlgorithm"/>-->
        <rdb:data-source id="dataSource">
            <rdb:sharding-rule data-sources="ds_2">
                <rdb:table-rules>
                    <rdb:table-rule logic-table="user" actual-tables="user_${0..3}" table-strategy="userTableStrategy"/>
                </rdb:table-rules>
                <rdb:default-database-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.database.NoneDatabaseShardingAlgorithm"/>
            </rdb:sharding-rule>
        </rdb:data-source>
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
        
    </beans>
    

    上面在使用分表的时候使用的是inline表达式.还有一种自定义表达式,上面是注释掉的,使用的是类来进行分表,但是我测试过程一直是类型转换异常,Integer转不成Long,这个错误清除,不知道发生在哪,因为着急,就不仔细研究了,下面把自定义表达式的类贴出来,有兴趣的可以试试.

    public class UserSingleKeyTableShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long> {
    
    	@Override
    	public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
    		 for (String each : availableTargetNames) {
    	            if (each.endsWith(shardingValue.getValue() % 4 + "")) {
    	                return each;
    	            }
    	        }
    	        throw new IllegalArgumentException();
    	}
    
    	@Override
    	public Collection<String> doInSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
    		Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
    		for (Long value : shardingValue.getValues()) {
                for (String tableName : availableTargetNames) {
                    if (tableName.endsWith(value % 4 + "")) {
                        result.add(tableName);
                    }
                }
            }
            return result;
    	}
    
    	@Override
    	public Collection<String> doBetweenSharding(Collection<String> availableTargetNames,
    			ShardingValue<Long> shardingValue) {
    		Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
            Range<Long> range = (Range<Long>) shardingValue.getValueRange();
            for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
                for (String each : availableTargetNames) {
                    if (each.endsWith(i % 4 + "")) {
                        result.add(each);
                    }
                }
            }
            return result;
    	}
    }
    
    1. 编写controller
    @GetMapping("/saves")
    public String saves(){
    
        for (Long i = 1L; i <= 100L; i++) {
            User user = User.builder()
                    .name("王智" + i)
                    .city("运城")
                    .build();
            user.setId(i);
            userService.save(user);
            log.info("插入的数据为 {} " ,user);
    
        }
    
        return "ok";
    }
    

    这下就可以测试了,在开始的时候写的sql不是指明了表是User,我就非常疑惑这个是怎么替换为user_0~4的,这个是sharding0-jdbc自动帮我们做的,我觉得应该类似拦截器的实现吧,也没有细究,只知道有这么回事.

    分库分表

    前面说了单库分表,那分库分表呢?一样的实现.

    1. 创建数据库和表
    CREATE DATABASE `sharding_0` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
    CREATE DATABASE `sharding_1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
    
    // 在每一个数据库中都创建两张表
    CREATE TABLE `user_0`(
    	id bigint(64) not null,
    	city varchar(20) not null,
    	name varchar(20) not null,
    	PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    CREATE TABLE `user_1`(
    	id bigint(64) not null,
    	city varchar(20) not null,
    	name varchar(20) not null,
    	PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    1. 创建sharding-db-table.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:rdb="http://www.dangdang.com/schema/ddframe/rdb" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/context 
                            http://www.springframework.org/schema/context/spring-context.xsd 
                            http://www.dangdang.com/schema/ddframe/rdb 
                            http://www.dangdang.com/schema/ddframe/rdb/rdb.xsd 
                            ">
        <!-- inline表达式报错 -->
        <context:property-placeholder  ignore-unresolvable="true"/> 
                           
        <!-- 主数据 -->
        <bean id="ds_0" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" primary="true">
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/sharding_0?serverTimezone=UTC&amp;characterEncoding=utf-8" />
            <property name="username" value="root" />
            <property name="password" value="nrblwbb7" />
        </bean>
        
     	<bean id="ds_1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/sharding_1?serverTimezone=UTC&amp;characterEncoding=utf-8" />
            <property name="username" value="root" />
            <property name="password" value="nrblwbb7" />
        </bean>
    
        <!--数据库按照城市划分,一个城市一个数据库-->
        <rdb:strategy id="databaseShardingStrategyHouseLouDong" sharding-columns="city" algorithm-class="com.sharding.shardingjdbcdemo.SingleKeyDbShardingAlgorithm"/>
        <!--数据库的表按照id划分,奇数id存1,偶数id存0-->
        <rdb:strategy id="tableShardingStrategyHouseLouDong" sharding-columns="id" algorithm-expression="user_${id.longValue() % 2}" />
    
        <rdb:data-source id="dataSource">
            <rdb:sharding-rule data-sources="ds_0, ds_1">
                <rdb:table-rules>
                    <rdb:table-rule logic-table="user" actual-tables="user_${0..1}" database-strategy="databaseShardingStrategyHouseLouDong" table-strategy="tableShardingStrategyHouseLouDong">
                    	<rdb:generate-key-column column-name="id"/>
                    </rdb:table-rule>
                </rdb:table-rules>
            </rdb:sharding-rule>
        </rdb:data-source>
    
    
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    </beans>
    
    1. 添加数据库的分库策略
    public class SingleKeyDbShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<String>  {
    
    	 private static Map<String, List<String>> shardingMap = new ConcurrentHashMap<>();
    	    
    	    static {
    	    	shardingMap.put("ds_0", Arrays.asList("山西"));
    	    	shardingMap.put("ds_1", Arrays.asList("陕西"));
    	    }
    	    
    	    @Override
    	    public String doEqualSharding(final Collection<String> availableTargetNames, final ShardingValue<String> shardingValue) {
    	        for (String each : availableTargetNames) {
    	        	if (shardingMap.get(each).contains(shardingValue.getValue())) {
    	        		 return each;
    	        	}
    	        }
    	        return "ds_0";
    	    }
    	    
    	    @Override
    	    public Collection<String> doInSharding(final Collection<String> availableTargetNames, final ShardingValue<String> shardingValue) {
    	    	Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
    	    	for (String each : availableTargetNames) {
    	         	if (shardingMap.get(each).contains(shardingValue.getValue())) {
    	         		result.add(each);
    	         	} else {
    	         		result.add("ds_0");
    	         	}
    	        }
    	        return result;
    	    }
    	    
    	    @Override
    	    public Collection<String> doBetweenSharding(final Collection<String> availableTargetNames, final ShardingValue<String> shardingValue) {
    	    	Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
    	    	for (String each : availableTargetNames) {
    	         	if (shardingMap.get(each).contains(shardingValue.getValue())) {
    	         		result.add(each);
    	         	} else {
    	         		result.add("ds_0");
    	         	}
    	        }
    	        return result;
    	    }
    
    }
    
    1. 修改controller中的saves方法,进行测试:
     @GetMapping("/saves")
    public String saves(){
    
        for (Long i = 1L; i <= 100L; i++) {
            User user = User.builder()
                    .name("王智" + i)
                    .city("山西")
                    .build();
            user.setId(i);
            userService.save(user);
            log.info("插入的数据为 {} " ,user);
    
        }
        for (Long i = 1L; i <= 100L; i++) {
            User user = User.builder()
                    .name("王智" + i)
                    .city("陕西")
                    .build();
            user.setId(i);
            userService.save(user);
            log.info("插入的数据为 {} " ,user);
        }
        return "ok";
    }
    

    这个是基于jdbc做的分库分表,对于spring,springboot下有不同的方法,参考 https://shardingsphere.apache.org/document/current/cn/manual/sharding-jdbc/usage/sharding/

    分布式主键的使用

    为了保证插入的主键不重复,所以使用分布式主键,其实在前面的xml中已经添加了实现<rdb:generate-key-column column-name="id"/>,接下来只要修改saves方法和save方法的实现就可以,也就是不给id赋值,并且插入的时候不给id字段,不过我在实践过程中发现生成的id全是偶数,不知道是不是偶然,如果不是,那么就需要重新找算法或者重新写分配策略了.

    基本就先这样了,后面有需要的进一步研究,还是看官方文档比较好 https://shardingsphere.apache.org/document/current/cn/features/sharding/

    上面的例子都亲身实践过,有问题可以私聊我,我是看了http://cxytiandi.com/course/15 这个视频课还有官方文档来写的,视频里用的是作者是进一步封装了jdbcTemplate,笔者用的是jdbcTemplate.

  • 相关阅读:
    Chap2: question: 1
    资格赛:题目3:格格取数
    资格赛:题目2:大神与三位小伙伴
    资格赛:题目1:同构
    最大流问题
    webpack(5)配置打包less和sass
    webpack(4)配置打包css
    C++进阶知识点(3)类的静态成员 字符和数字的互转 lambda
    ubuntu shell 监控某个进程占用的资源
    webpack(4)配置打包多个html
  • 原文地址:https://www.cnblogs.com/wadmwz/p/10481413.html
Copyright © 2011-2022 走看看