springboot+jpa分庫分表項目實例

分庫分表場景

關係型數據庫自己比較容易成爲系統瓶頸,單機存儲容量、鏈接數、處理能力都有限。當單表的數據量達到1000W或100G之後,因爲查詢維度較多,即便添加從庫、優化索引,作不少操做時性能仍降低嚴重。此時就要考慮對其進行切分了,切分的目的就在於減小數據庫的負擔,縮短查詢時間。java

分庫分表用於應對當前互聯網常見的兩個場景——大數據量和高併發。一般分爲垂直拆分和水平拆分兩種。mysql

垂直拆分是根據業務將一個庫(表)拆分爲多個庫(表)。如:將常常和不常訪問的字段拆分至不一樣的庫或表中。因爲與業務關係密切,目前的分庫分表產品均使用水平拆分方式。git

水平拆分則是根據分片算法將一個庫(表)拆分爲多個庫(表)。如:按照ID的最後一位以3取餘,尾數是1的放入第1個庫(表),尾數是2的放入第2個庫(表)等。github

單純的分表雖然能夠解決數據量過大致使檢索變慢的問題,但沒法解決過多併發請求訪問同一個庫,致使數據庫響應變慢的問題。因此一般水平拆分都至少要採用分庫的方式,用於一併解決大數據量和高併發的問題。這也是部分開源的分片數據庫中間件只支持分庫的緣由。web

但分表也有不可替代的適用場景。最多見的分表需求是事務問題。同在一個庫則不需考慮分佈式事務,善於使用同庫不一樣表可有效避免分佈式事務帶來的麻煩。目前強一致性的分佈式事務因爲性能問題,致使使用起來並不必定比不分庫分錶快。目前採用最終一致性的柔性事務居多。分表的另外一個存在的理由是,過多的數據庫實例不利於運維管理。綜上所述,最佳實踐是合理地配合使用分庫+分表。算法

Sharding-JDBC簡介

Sharding-JDBC是噹噹應用框架ddframe中,從關係型數據庫模塊dd-rdb中分離出來的數據庫水平分片框架,實現透明化數據庫分庫分表訪問。Sharding-JDBC是繼dubbox和elastic-job以後,ddframe系列開源的第3個項目。spring

定位爲輕量級Java框架,在Java的JDBC層提供的額外服務。 它使用客戶端直連數據庫,以jar包形式提供服務,無需額外部署和依賴,可理解爲加強版的JDBC驅動,徹底兼容JDBC和各類ORM框架。sql

  • 適用於任何基於Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 基於任何第三方的數據庫鏈接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意實現JDBC規範的數據庫。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

Sharding-JDBC分片策略靈活,可支持等號、between、in等多維度分片,也可支持多分片鍵。數據庫

SQL解析功能完善,支持聚合、分組、排序、limit、or等查詢,並支持Binding Table以及笛卡爾積表查詢。json

 

項目實踐

數據準備

準備兩個數據庫。並在兩個庫中建好表, 建表sql以下:

DROP TABLE IF EXISTS `user_auth_0`;
CREATE TABLE `user_auth_0` (
  `user_id` bigint(20) NOT NULL,
  `add_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `email` varchar(16) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `phone` varchar(16) DEFAULT NULL,
  `remark` varchar(16) DEFAULT NULL,
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `USER_AUTH_PHONE` (`phone`),
  UNIQUE KEY `USER_AUTH_EMAIL` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


DROP TABLE IF EXISTS `user_auth_1`;
CREATE TABLE `user_auth_1` (
  `user_id` bigint(20) NOT NULL,
  `add_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `email` varchar(16) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `phone` varchar(16) DEFAULT NULL,
  `remark` varchar(16) DEFAULT NULL,
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `USER_AUTH_PHONE` (`phone`),
  UNIQUE KEY `USER_AUTH_EMAIL` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

POM配置

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

       <!-- 引入jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
       <!-- 引入mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>
        <!-- sharding-jdbc -->
        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>sharding-jdbc-core</artifactId>
            <version>1.5.4</version>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

application.yml配置

spring:
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
database0:
  driverClassName: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/mazhq?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
  username: root
  password: 123456
  databaseName: mazhq

database1:
  driverClassName: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/liugh?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
  username: root
  password: 123456
  databaseName: liugh

分庫分表最主要有幾個配置

 1. 有多少個數據源 (2個:database0和database1)

@Data
@ConfigurationProperties(prefix = "database0")
@Component
public class Database0Config {
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    private String databaseName;

    public DataSource createDataSource() {
        DruidDataSource result = new DruidDataSource();
        result.setDriverClassName(getDriverClassName());
        result.setUrl(getUrl());
        result.setUsername(getUsername());
        result.setPassword(getPassword());
        return result;
    }
}

2. 用什麼列進行分庫以及分庫算法 (通常是用具體值對2取餘判斷入哪一個庫,我採用的是判斷值是否大於20)

@Component
public class DatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<Long> {
    @Autowired
    private Database0Config database0Config;
    @Autowired
    private Database1Config database1Config;
    @Override
    public String doEqualSharding(Collection<String> collection, ShardingValue<Long> shardingValue) {
        Long value = shardingValue.getValue();
        if (value <= 20L) {
            return database0Config.getDatabaseName();
        } else {
            return database1Config.getDatabaseName();
        }
    }

    @Override
    public Collection<String> doInSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        for (Long value : shardingValue.getValues()) {
            if (value <= 20L) {
                result.add(database0Config.getDatabaseName());
            } else {
                result.add(database1Config.getDatabaseName());
            }
        }
        return result;
    }

    @Override
    public Collection<String> doBetweenSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
        Range<Long> range = shardingValue.getValueRange();
        for (Long value = range.lowerEndpoint(); value <= range.upperEndpoint(); value++) {
            if (value <= 20L) {
                result.add(database0Config.getDatabaseName());
            } else {
                result.add(database1Config.getDatabaseName());
            }
        }
        return result;
    }
}

3. 用什麼列進行分表以及分表算法

@Component
public class TableShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long> {
    @Override
    public String doEqualSharding(Collection<String> tableNames, ShardingValue<Long> shardingValue) {
        for (String each : tableNames) {
            if (each.endsWith(shardingValue.getValue() % 2 + "")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }

    @Override
    public Collection<String> doInSharding(Collection<String> tableNames, ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(tableNames.size());
        for (Long value : shardingValue.getValues()) {
            for (String tableName : tableNames) {
                if (tableName.endsWith(value % 2 + "")) {
                    result.add(tableName);
                }
            }
        }
        return result;
    }

    @Override
    public Collection<String> doBetweenSharding(Collection<String> tableNames, ShardingValue<Long> shardingValue) {
        Collection<String> result = new LinkedHashSet<>(tableNames.size());
        Range<Long> range = shardingValue.getValueRange();
        for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
            for (String each : tableNames) {
                if (each.endsWith(i % 2 + "")) {
                    result.add(each);
                }
            }
        }
        return result;
    }
}

4. 每張表的邏輯表名和全部物理表名和集成調用

@Configuration
public class DataSourceConfig {
    @Autowired
    private Database0Config database0Config;

    @Autowired
    private Database1Config database1Config;

    @Autowired
    private DatabaseShardingAlgorithm databaseShardingAlgorithm;

    @Autowired
    private TableShardingAlgorithm tableShardingAlgorithm;

    @Bean
    public DataSource getDataSource() throws SQLException {
        return buildDataSource();
    }

    private DataSource buildDataSource() throws SQLException {
        //分庫設置
        Map<String, DataSource> dataSourceMap = new HashMap<>(2);
        //添加兩個數據庫database0和database1
        dataSourceMap.put(database0Config.getDatabaseName(), database0Config.createDataSource());
        dataSourceMap.put(database1Config.getDatabaseName(), database1Config.createDataSource());
        //設置默認數據庫
        DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, database0Config.getDatabaseName());

        //分表設置,大體思想就是將查詢虛擬表Goods根據必定規則映射到真實表中去
        TableRule orderTableRule = TableRule.builder("user_auth")
                .actualTables(Arrays.asList("user_auth_0", "user_auth_1"))
                .dataSourceRule(dataSourceRule)
                .build();

        //分庫分表策略
        ShardingRule shardingRule = ShardingRule.builder()
                .dataSourceRule(dataSourceRule)
                .tableRules(Arrays.asList(orderTableRule))
                .databaseShardingStrategy(new DatabaseShardingStrategy("user_id", databaseShardingAlgorithm))
                .tableShardingStrategy(new TableShardingStrategy("user_id", tableShardingAlgorithm)).build();
        DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);
        return dataSource;
    }


    @Bean
    public KeyGenerator keyGenerator() {
        return new DefaultKeyGenerator();
    }

接口測試代碼

一、實體類

/**
 * @author mazhq
 * @date 2019/7/30 16:41
 */
@Entity
@Data
@Table(name = "USER_AUTH", uniqueConstraints = {@UniqueConstraint(name = "USER_AUTH_PHONE", columnNames = {"PHONE"}),
@UniqueConstraint(name = "USER_AUTH_EMAIL", columnNames = {"EMAIL"})})
public class UserAuthEntity implements Serializable {
    private static final long serialVersionUID = 7230052310725727465L;
    @Id
    private Long userId;
    @Column(name = "PHONE", length = 16)
    private String phone;
    @Column(name = "EMAIL", length = 16)
    private String email;
    private String password;
    @Column(name = "REMARK",length = 16)
    private String remark;
    @Column(name = "ADD_DATE", nullable = false, columnDefinition = "datetime default now()")
    private Date addDate;
}

 2. Dao層

@Repository
public interface UserAuthDao extends JpaRepository<UserAuthEntity, Long> {
}

 3. controller層

/**
 * @author mazhq
 * @Title: UserAuthController
 * @date 2019/8/1 17:18
 */
@RestController
@RequestMapping("/user")
public class UserAuthController {
    @Autowired
    private UserAuthDao userAuthDao;

    @PostMapping("/save")
    public String save(){
        for (int i=0;i<40;i++) {
            UserAuthEntity userAuthEntity = new UserAuthEntity();
            userAuthEntity.setUserId((long)i);
            userAuthEntity.setAddDate(new Date());
            userAuthEntity.setEmail("test"+i+"@163.com");
            userAuthEntity.setPassword("123456");
            userAuthEntity.setPhone("1388888888"+i);
            Random r = new Random();
            userAuthEntity.setRemark(""+r.nextInt(100));
            userAuthDao.save(userAuthEntity);
        }
        return "success";
    }

    @PostMapping("/select")
    public String select(){
        return JSONObject.toJSONString(userAuthDao.findAll(Sort.by(Sort.Order.desc("remark"))));
    }
}  

測試方式:

先調用:http://localhost:8080/user/save

再查詢:http://localhost:8080/user/select

git地址:sharding

相關文章
相關標籤/搜索