ShardingSphere 简介
官网:https://shardingsphere.apache.org/index_zh.html Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的数据水平扩展、分布式事务和分布式治理等功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
注意事项:
支持项:
- 提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用;
- 独立使用读写分离支持SQL透传;
- 同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性;
- 基于Hint的强制主库路由。
不支持项:
- 主库和从库的数据同步;
- 主库和从库的数据同步延迟导致的数据不一致;
- 主库双写或多写;
- 跨主库和从库之间的事务的数据不一致。主从模型中,事务中读写均用主库。
本文目标
Spring Boot 2.x 整合 ShardingSphere 5.x 实现MySQL读写分离(一主二从),
注:MySQL主从复制请参考其他资料自行实现。
开发环境
IDEA + JDK11 + Maven + MySQL 8.x + Spring Boot 2.x
MySQL数据准备
- 主库(数据的增删改)与从库(数据的查询):
- 数据库表:
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户姓名',
`user_age` tinyint(4) DEFAULT NULL COMMENT '用户年龄',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
为了演示直观,可以先初始化从库数据:
从库1:
从库2:
代码实操
- 创建Spring Boot工程,项目结构
- 引入Maven依赖
(因为是Spring Boot项目,所以我们优选spring-boot-starter依赖)
<!-- ShardingSphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.0.0-alpha</version>
</dependency>
- 配置application.yml
spring:
shardingsphere:
# 数据源配置
datasource:
common:
# 数据库驱动类名
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接池类名称
type: com.zaxxer.hikari.HikariDataSource
# 数据源名称,多数据源以逗号分隔
names: main,slave1,slave2
main:
jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere-main?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
slave1:
jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere-slave1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
slave2:
jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere-slave2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
rules:
replica-query:
# 负载均衡算法配置(只配置一种也可以)
load-balancers:
# 轮询算法(此处是枚举)(名称自定义)
round-robin:
type: ROUND_ROBIN
props:
# 工作机器唯一标识
worker-id: 1
# 随机访问算法(此处是枚举)(名称自定义)
random:
type: RANDOM
props:
# 工作机器唯一标识
worker-id: 1
data-sources:
# 名称(名称自定义)
main-slave:
# 写数据源名称
primary-data-source-name: main
# 读数据源名称,多个从数据源用逗号分隔
replica-data-source-names: slave1,slave2
# 负载均衡算法名称(把枚举的名称填写到这里)
load-balancer-name: round-robin
# 属性配置
props:
# 展示修改以后的sql语句
sql-show: true
- 业务层代码
UserServiceImpl 类
package com.wjh.service.impl;
import com.wjh.dto.input.UserInputDTO;
import com.wjh.entity.User;
import com.wjh.repository.UserRepository;
import com.wjh.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* 用户表 业务层接口实现类
*
* @author Jiahai
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional(rollbackFor = Exception.class)
public void initData() {
Date date = new Date();
this.userRepository.saveAll(
List.of(
new User().setUserName("张三").setUserAge(16).setCreateTime(date),
new User().setUserName("李四").setUserAge(17).setCreateTime(date),
new User().setUserName("王五").setUserAge(18).setCreateTime(date),
new User().setUserName("喜羊羊").setUserAge(19).setCreateTime(date),
new User().setUserName("灰太狼").setUserAge(20).setCreateTime(date)
)
);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(UserInputDTO userInputDTO) {
this.userRepository.save(
new User()
.setUserName(userInputDTO.getUserName())
.setUserAge(userInputDTO.getUserAge())
);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Integer userId) {
this.userRepository.deleteById(userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(Integer userId, UserInputDTO userInputDTO) {
User user = this.findByUserId(userId);
if (!Objects.equals(user.getUserName(), userInputDTO.getUserName()) || !Objects.equals(user.getUserAge(), userInputDTO.getUserAge())) {
BeanUtils.copyProperties(user, userInputDTO);
}
}
@Override
public User findByUserId(Integer userId) {
return this.userRepository.findById(userId).orElseThrow(() -> new RuntimeException("用户不存在"));
}
@Override
public List<User> list() {
return this.userRepository.findAll();
}
}
UserController类
package com.wjh.controller;
import com.wjh.dto.input.UserInputDTO;
import com.wjh.entity.User;
import com.wjh.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户表 API
*
* @author Jiahai
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 初始化数据到数据库中
*/
@PostMapping("/initData")
public void initDate() {
userService.initData();
}
/**
* 新增用户
*
* @param userInputDTO 入参
*/
@PostMapping
public void save(@RequestBody UserInputDTO userInputDTO) {
userService.save(userInputDTO);
}
/**
* 根据主键删除用户
*
* @param userId 主键
*/
@DeleteMapping("/{userId}")
public void delete(@PathVariable Integer userId) {
userService.delete(userId);
}
/**
* 修改用户
*
* @param userId 用户ID
* @param userInputDTO 入参
*/
@PutMapping("/{userId}")
public void update(@PathVariable Integer userId, @RequestBody UserInputDTO userInputDTO) {
userService.update(userId, userInputDTO);
}
/**
* 根据主键查询
*
* @param userId
* @return
*/
@GetMapping("/{userId}")
public User findByUserId(@PathVariable Integer userId) {
return userService.findByUserId(userId);
}
/**
* 列表查询
*
* @return
*/
@GetMapping("/list")
public List<User> list() {
return userService.list();
}
}
- 效果演示(yml中配置了从库采用轮询算法)
调用初始化数据接口 localhost:8080/user/initData(将对主库操作)
主库数据:
调用查询接口 localhost:8080/user/list(将对从库操作)
再次调用查询接口 localhost:8080/user/list(将对从库操作)