分庫分表中間件sharding-jdbc的使用

數據分片產生的背景,能夠查看https://shardingsphere.apache.org/document/current/cn/features/sharding/,包括了垂直拆分和水平拆分的概念.還有這個框架的目標是什麼,都寫得很清楚java

Sharding-JDBC與MyCat:mysql

  • 解決分庫分表的中間件.
  • 可是定位不一樣,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/git

功能:github

  • 分庫分表:
    • 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進行讀寫分離實戰

在數據庫的操做中,寫操做是很是耗時的,而最經常使用的是讀操做,讀寫分離的目的是避免數據庫的寫操做影響讀操做的效率.最重要的目的仍是減小數據庫的壓力,提升性能.web

這只是模仿讀寫分析實戰,流程是建立兩個數據庫,配置兩個數據源,一個是主表,一個是從表,寫修改刪除在主表,查詢是在從表.算法

  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,說明插入(寫)操做在主表,查詢在從表.spring

強制路由

覺得在主表和從表之間同步是須要時間的,因此有的時候在寫完以後就要當即進行讀操做,因此這個時候就須要強制路由,讓從主表中讀取.sql

ShardingSphere使用ThreadLocal管理分片鍵值。能夠經過編程的方式向HintManager中添加分片條件,該分片條件僅在當前線程內生效。數據庫

HintManager.getInstance().setMasterRouteOnly();

在查詢前使用這句能夠指定從主庫中進行讀取數據.express

分片算法

參考: http://www.javashuo.com/article/p-zuviydej-bn.html

  1. 分片枚舉: 經過在配置文件中配置可能的枚舉id,本身配置分片。 這種規則適用於特定的場景,好比有些業務須要按照省份或區縣來作保存,而全國的省份區縣固定的,這類業務使用這一規則。
  2. 範圍約定: 此分片適用於提早規劃好分片字段某個範圍屬於哪一個分片. 這個接觸過,就是好比說id在1~10000的在一張表,10001~20000在另外一張表.
  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.

相關文章
相關標籤/搜索