分庫分表實(shí)踐-springboot+mybatis+druid+shardingsphere

0 前言

0.1 Sharding-Sphere

Sharding-Sphere是一套開源的分布式數(shù)據(jù)庫中間件解決方案組成的生態(tài)圈执虹,它由Sharding-JDBC榛了、Sharding-Proxy和Sharding-Sidecar這3款相互獨(dú)立的產(chǎn)品組成停巷。他們均提供標(biāo)準(zhǔn)化的數(shù)據(jù)分片硅卢、讀寫分離仁期、柔性事務(wù)和數(shù)據(jù)治理功能,可適用于如Java同構(gòu)懂盐、異構(gòu)語言褥赊、容器、云原生等各種多樣化的應(yīng)用場景莉恼。
Sharding-Sphere定位為關(guān)系型數(shù)據(jù)庫中間件拌喉,旨在充分合理地在分布式的場景下利用關(guān)系型數(shù)據(jù)庫的計(jì)算和存儲(chǔ)能力翼岁,而并非實(shí)現(xiàn)一個(gè)全新的關(guān)系型數(shù)據(jù)庫。

0.2 Sharding-JDBC

我們這邊主要用的是里面的Sharding-JDBC:

定位為輕量級(jí)Java框架司光,在Java的JDBC層提供的額外服務(wù)琅坡。 它使用客戶端直連數(shù)據(jù)庫,以jar包形式提供服務(wù)残家,無需額外部署和依賴榆俺,可理解為增強(qiáng)版的JDBC驅(qū)動(dòng),完全兼容JDBC和各種ORM框架坞淮。

  • 適用于任何基于Java的ORM框架茴晋,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 基于任何第三方的數(shù)據(jù)庫連接池回窘,如:DBCP, C3P0, BoneCP, Druid, HikariCP等诺擅。
  • 支持任意實(shí)現(xiàn)JDBC規(guī)范的數(shù)據(jù)庫。目前支持MySQL啡直,Oracle烁涌,SQLServer和PostgreSQL。
系統(tǒng)架構(gòu)圖
  1. 配置
    1.1 springboot+mybatis+druid

    1.2 sharding-spheres
    pom.xml引入:
<dependency>
    <groupId>io.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>3.0.0.M2</version>
</dependency>

1.3 分片設(shè)計(jì)

邏輯表結(jié)構(gòu):

CREATE TABLE `t_order` (
  `order_id` int(11) NOT NULL,
  `user_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`order_id`)
)

按照order_id進(jìn)行范圍分片(order_id < 100的在shard1酒觅,其他的在shard2)撮执;
再按user_id進(jìn)行hash分片(user_id % 2 分表落在t_order_0和t_order_1上)

數(shù)據(jù)庫結(jié)構(gòu)如下圖:

數(shù)據(jù)庫結(jié)構(gòu)

application.properties

server.port=9001

mybatis.mapper-locations=classpath:mapper/*.xml

sharding.jdbc.datasource.names=ds0,ds1

sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc:mysql://localhost:3306/shard1?useSSL=false
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=password

sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc:mysql://localhost:3306/shard2?useSSL=false
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=password

sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1}
sharding.jdbc.config.sharding.tables.t_order.database-strategy.standard.sharding-column=order_id
sharding.jdbc.config.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.joey.springboot.shardingdemo.conf.OrderShardingAlgorithm
sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id
sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2}
sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=order_id

sharding.jdbc.config.sharding.props.sql.show=true

具體的配置參數(shù)含義見官方文檔
http://shardingsphere.io/document/current/cn/manual/sharding-jdbc/configuration/config-spring-boot/

其中OrderShardingAlgorithm配置類為:

package com.joey.springboot.shardingdemo.conf;

import io.shardingsphere.core.api.algorithm.sharding.PreciseShardingValue;
import io.shardingsphere.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm;

import java.util.Collection;

public class OrderShardingAlgorithm implements PreciseShardingAlgorithm {

    @Override
    public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
        if(shardingValue.getValue().compareTo(100) > 0) {
            return "ds1";
        }
        return "ds0";
    }
}

  1. 測試代碼

controller:

package com.joey.springboot.shardingdemo.controller;

import com.joey.springboot.shardingdemo.dao.Order;
import com.joey.springboot.shardingdemo.service.OrderService;
import io.shardingsphere.core.jdbc.core.connection.ShardingConnection;
import io.shardingsphere.core.jdbc.core.transaction.TransactionLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.sql.DataSource;
import java.sql.SQLException;

@RestController
public class OrderController {

    @Autowired
    OrderService orderService;

    @Autowired
    DataSource dataSource;

    @RequestMapping("/order/{id}")
    public Order getOrderById(@PathVariable int id) {
        System.out.println(orderService.getOrderById(id));
        return orderService.getOrderById(id);
    }

    @RequestMapping(value="/order", method= RequestMethod.POST)
    @ResponseBody
    @Transactional(rollbackFor = Exception.class)
    public Order addOrder(@RequestBody Order order) {
        orderService.addOrder(order);
        Order order1 = new Order();
        order1.setOrderId(order.getOrderId()+100);
        order1.setUserId(order.getUserId()+1);
        orderService.addOrder(order1);
        return orderService.getOrderById(order.getOrderId());
    }
}

service:

package com.joey.springboot.shardingdemo.service;

import com.joey.springboot.shardingdemo.dao.Order;
import com.joey.springboot.shardingdemo.dao.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public Order getOrderById(int id) {
        return orderMapper.selectByPrimaryKey(id);
    }


    public int addOrder(Order order) {
        return orderMapper.insert(order);
    }
}

model:

package com.joey.springboot.shardingdemo.dao;

public class Order {
    private Integer orderId;

    private Integer userId;

    public Integer getOrderId() {
        return orderId;
    }

    public void setOrderId(Integer orderId) {
        this.orderId = orderId;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }
}

mapper:

package com.joey.springboot.shardingdemo.dao.mapper;

import com.joey.springboot.shardingdemo.dao.Order;

public interface OrderMapper {
    int deleteByPrimaryKey(Integer orderId);

    int insert(Order record);

    int insertSelective(Order record);

    Order selectByPrimaryKey(Integer orderId);

    int updateByPrimaryKeySelective(Order record);

    int updateByPrimaryKey(Order record);
}

mapper.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.joey.springboot.shardingdemo.dao.mapper.OrderMapper">
  <resultMap id="BaseResultMap" type="com.joey.springboot.shardingdemo.dao.Order">
    <id column="order_id" jdbcType="INTEGER" property="orderId" />
    <result column="user_id" jdbcType="INTEGER" property="userId" />
  </resultMap>
  <sql id="Base_Column_List">
    order_id, user_id
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_order
    where order_id = #{orderId,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    delete from t_order
    where order_id = #{orderId,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.joey.springboot.shardingdemo.dao.Order">
    insert into t_order (order_id, user_id)
    values (#{orderId,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER})
  </insert>
  <insert id="insertSelective" parameterType="com.joey.springboot.shardingdemo.dao.Order">
    insert into t_order
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="orderId != null">
        order_id,
      </if>
      <if test="userId != null">
        user_id,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="orderId != null">
        #{orderId,jdbcType=INTEGER},
      </if>
      <if test="userId != null">
        #{userId,jdbcType=INTEGER},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.joey.springboot.shardingdemo.dao.Order">
    update t_order
    <set>
      <if test="userId != null">
        user_id = #{userId,jdbcType=INTEGER},
      </if>
    </set>
    where order_id = #{orderId,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.joey.springboot.shardingdemo.dao.Order">
    update t_order
    set user_id = #{userId,jdbcType=INTEGER}
    where order_id = #{orderId,jdbcType=INTEGER}
  </update>
</mapper>

這樣可以方便地使用@Transactional注解實(shí)現(xiàn)弱XA

  • 完全支持非跨庫事務(wù),例如:僅分表舷丹,或分庫但是路由的結(jié)果在單庫中抒钱。
  • 完全支持因邏輯異常導(dǎo)致的跨庫事務(wù)。例如:同一事務(wù)中颜凯,跨兩個(gè)庫更新谋币。更新完畢后,拋出空指針症概,則兩個(gè)庫的內(nèi)容都能回滾蕾额。
  • 不支持因網(wǎng)絡(luò)、硬件異常導(dǎo)致的跨庫事務(wù)穴豫。例如:同一事務(wù)中凡简,跨兩個(gè)庫更新,更新完畢后精肃、未提交之前,第一個(gè)庫宕機(jī)帜乞,則只有第二個(gè)庫數(shù)據(jù)提交司抱。

3.問題

3.1問題描述

版本號(hào)
springboot 1.5.10.RELEASE
sharding-jdbc 3.0.0.M2

用spring自帶的@Transactional 報(bào)錯(cuò):

Could not dispatch event: io.shardingsphere.core.transaction.listener.TransactionListener@6e43ddd6 to public void io.shardingsphere.core.transaction.listener.TransactionListener.listen(io.shardingsphere.core.transaction.event.TransactionEvent) throws java.sql.SQLException

3.2問題原因

觸發(fā)事務(wù)的時(shí)候會(huì)調(diào)用TransactionListener.listen中的:

TransactionManager transactionManager = TransactionContextHolder.get().getTransactionManager();

從ThreadLocal中獲得的TransactionContext中的transactionManager為空
springboot啟動(dòng)的時(shí)候,加載了TransactionLoader黎烈,里面的doXaTransactionConfiguration是set了ThreadLocal中的transactionManager习柠,但是set只對當(dāng)前線程的ThreadLocal變量有效匀谣,所以導(dǎo)致在有新線程起來的時(shí)候再初始化TransactionContext時(shí),transactionManager就為空资溃。

3.3 問題解決

修改源碼中的TransactionContext類

    private TransactionManager transactionManager = new WeakXaTransactionManager();

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末武翎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子溶锭,更是在濱河造成了極大的恐慌宝恶,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趴捅,死亡現(xiàn)場離奇詭異垫毙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)拱绑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門综芥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人猎拨,你說我怎么就攤上這事膀藐。” “怎么了红省?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵消请,是天一觀的道長。 經(jīng)常有香客問我类腮,道長臊泰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任蚜枢,我火速辦了婚禮缸逃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘厂抽。我一直安慰自己需频,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布筷凤。 她就那樣靜靜地躺著昭殉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪藐守。 梳的紋絲不亂的頭發(fā)上挪丢,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音卢厂,去河邊找鬼乾蓬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛慎恒,可吹牛的內(nèi)容都是我干的任内。 我是一名探鬼主播撵渡,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼死嗦!你這毒婦竟也來了趋距?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對情侶失蹤越除,失蹤者是張志新(化名)和其女友劉穎节腐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體廊敌,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铜跑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了骡澈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锅纺。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肋殴,靈堂內(nèi)的尸體忽然破棺而出囤锉,到底是詐尸還是另有隱情,我是刑警寧澤护锤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布官地,位于F島的核電站,受9級(jí)特大地震影響烙懦,放射性物質(zhì)發(fā)生泄漏驱入。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一氯析、第九天 我趴在偏房一處隱蔽的房頂上張望亏较。 院中可真熱鬧,春花似錦掩缓、人聲如沸雪情。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽巡通。三九已至,卻和暖如春舍哄,著一層夾襖步出監(jiān)牢的瞬間宴凉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國打工蠢熄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留跪解,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓签孔,卻偏偏與公主長得像叉讥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子饥追,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容