SpringBoot-Hikari-Mybatis集成,整合sharding-jdbc,实现分库分表

一、重点技术概述

为何要使用HiKari连接池?

字节码精简:优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码;
优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一;
自定义数组类型(FastStatementList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描;
自定义集合类型(ConcurrentBag):提高并发读写的效率;
其他针对BoneCP缺陷的优化,比如对于耗时超过一个CPU时间片的方法调用的研究(但没说具体怎么优化)。

怎么使用Hikari连接池呢?

springboot版本是2.X,当使用spring-boot-starter-jdbc或者spring-boot-starter-data-jpa依赖,springboot就会自动引入HikariCP的依赖。

水平分割原理

1、水平分库

1)、概念:
以字段为依据,按照一定策略,将一个库中的数据拆分到多个库中。
2)、结果
每个库的结构都一样;数据都不一样;
所有库的并集是全量数据;

2、水平分表

1)、概念
以字段为依据,按照一定策略,将一个表中的数据拆分到多个表中。
2)、结果
每个表的结构都一样;数据都不一样;
所有表的并集是全量数据;

水平分割使用Shard-jdbc中间件

1、架构图

SpringBoot-Hikari-Mybatis集成,整合sharding-jdbc,实现分库分表_第1张图片

2、特点

1)、Sharding-JDBC直接封装JDBC API,旧代码迁移成本几乎为零。
2)、适用于任何基于Java的ORM框架,如Hibernate、Mybatis等 。
3)、可基于任何第三方的数据库连接池,如DBCP、C3P0、 BoneCP、Druid等。
4)、以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖。
5)、分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。
6)、SQL解析功能完善,支持聚合、分组、排序、limit、or等查询。

二、代码整合

1、application.yml

spring:
  messages:
    encoding: utf-8
    cache-seconds: 100000
  profiles:
    active: beta

2、数据库分库策略

/**
 * 分库算法:编号后两位
* * @author zhoudoujun01 * @date 2019年8月6日14:55:06 */ public class DatabaseShardingAlgorithm implements ComplexKeysShardingAlgorithm { public Collection doSharding(Collection availableTargetNames, Collection shardingValues) { // 根据分片键名取得分片值列表 Collection orderIdValues = getShardingValue(shardingValues, Constant.COL_NAME_ID); // Collection dsList = new LinkedHashSet(availableTargetNames.size()); // 优先按照pid分库 if (orderIdValues != null && orderIdValues.size() > 0) { for (String orderId : orderIdValues) { String dsName = getDsNameById(orderId, availableTargetNames); if (dsName != null && dsName.length() > 0) { dsList.add(dsName); } } } if (dsList.size() == 0) { throw new UnsupportedOperationException("分库失败(编号不符合规范)"); } return dsList; } /** * 根据编号取得数据源名:编号后两位 * * @param id * @param availableTargetNames * @return 数据源名 */ private String getDsNameById(String id, Collection availableTargetNames) { String dsName = null; for (String name : availableTargetNames) { // 调用编号规则 if (name.endsWith(DataSourceId.getDataSourceID(id))) { System.out.println("================== id: " + id + ", name: " + name); dsName = name; break; } } return dsName; } /** * 根据分片键名取得分片值列表 * * @param shardingValues * 分片值对象 * @param key * 分片键名 * @return 分片值列表 */ private Collection getShardingValue(Collection shardingValues, final String key) { Collection valueSet = new ArrayList(); Iterator iterator = shardingValues.iterator(); while (iterator.hasNext()) { ShardingValue next = iterator.next(); if (next instanceof ListShardingValue) { @SuppressWarnings("unchecked") ListShardingValue value = (ListShardingValue) next; if (value.getColumnName().equals(key)) { System.out.println("===================== key: " + key + ", values: " + value.getValues()); valueSet = value.getValues(); } } } return valueSet; } } /** * 产生编号 * * @return */ public static String buildID() { long seqId = nextId(); String id = String.format(SEQID_FORMAT, seqId); // 超过所占位数,截取后几位 if (id.length() > SEQID_DIGITS) { id = id.substring(id.length() - SEQID_DIGITS, id.length()); } // seqId后四位与数据源个数取模,得到数据源编号 int useridLast4 = (int) (seqId % ((int) Math.pow(10, USERID_LAST_DIGITS))); int ds = useridLast4 % DATASOURCE_CNT; // 时间 Calendar cal = Calendar.getInstance(); return String.format(ORDERNO_FORMAT, cal, id, ds); }

3、数据源集成配置

jdbc2.properties

test1.url=jdbc:mysql://localhost:3306/test1?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
test1.username=root
test1.password=root
test1.initialSize=5
test1.maxActive=5
test1.maxIdle=5
test1.minIdle=1
test1.maxWait=6000
test1.validationQuery=SELECT 1 FROM DUAL


test2.url=jdbc:mysql://localhost:3306/test2?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
test2.username=root
test2.password=root
test2.initialSize=5
test2.maxActive=5
test2.maxIdle=5
test2.minIdle=1
test2.maxWait=6000
test2.validationQuery=SELECT 1 FROM DUAL

ShardingDataSourceConfig

数据源集成配置

	@Bean
	@Primary
	public DataSource shardingDataSource() throws SQLException {
		ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
		// PERSON表分库规则
		shardingRuleConfig.getTableRuleConfigs().add(getPersonRuleConfiguration());

		return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new HashMap(),
				new Properties());
	}

PERSON表分库规则

	/**
	 * 定义Person表分库规则
	 * @return
	 */
	private TableRuleConfiguration getPersonRuleConfiguration() {
		TableRuleConfiguration paymentRequestTableRule = new TableRuleConfiguration();
		paymentRequestTableRule.setLogicTable(LOGIC_TABLE_PERSON);
		paymentRequestTableRule.setActualDataNodes(ACTUAL_DATA_NODES_PERSON);
		paymentRequestTableRule.setDatabaseShardingStrategyConfig(new ComplexShardingStrategyConfiguration(
				SHARDING_COLUMN_ID, DatabaseShardingAlgorithm.class.getName()));
		paymentRequestTableRule.setKeyGeneratorColumnName(KEY_GENERATOR_COLUMN_NAME_ID);
		return paymentRequestTableRule;
	}

组装数据源map
Prd 4个库
Beta 2个库
取系统环境beta

/**
	 * 系统环境名
	 */
	private static final String ACTIVE_ENV = "spring.profiles.active";
/**
	 * 多数据源别名列表
	 */
	private static String[] DS_NAMES = { "ds_00", "ds_01", "ds_02", "ds_03" };
/**
	 * 加载创建dataSource:
	 *
	 * @return
	 */
	private Map createDataSourceMap() {
		HashMap dataSourceMap = Maps.newHashMap();
		String activeEnvPrd = "prd";
		// 得到系统环境
		String activeEnv = env.getProperty(ACTIVE_ENV);
		log.info("dataSource 1 url:{} activeEnv:" + activeEnv + ","
				+ env.getProperty(MYSQL_DS_PREFIX + 1 + MYSQL_DS_SUFFIX_URL));

		// 未配置或者指定生产环境时,直接使用指定数据源配置
		if (activeEnv == null || activeEnvPrd.equalsIgnoreCase(activeEnv)) {
			for (int i = 1; i <= DS_NAMES.length; i++) {
				dataSourceMap.put(DS_NAMES[i - 1], getDataSourceItem(i));
			}
		}
		// 测试环境下,数据源00,01对应1数据库,数据源02,03对应2数据库
		else {
			for (int i = 1; i <= DS_NAMES.length; i++) {
				if (i <= 2) {
					dataSourceMap.put(DS_NAMES[i - 1], getDataSourceItem(1));
				} else if (i <= 4) {
					dataSourceMap.put(DS_NAMES[i - 1], getDataSourceItem(2));
				}
			}
		}
		return dataSourceMap;
	}

Hikari根据指定数据源配置标识生成数据源对象

private DataSource getDataSourceItem(int i) {
		HikariConfig config = new HikariConfig();
		config.setJdbcUrl(env.getProperty(MYSQL_DS_PREFIX + i + MYSQL_DS_SUFFIX_URL));
		config.setUsername(env.getProperty(MYSQL_DS_PREFIX + i + ".username"));
		config.setPassword(env.getProperty(MYSQL_DS_PREFIX + i + ".password"));
		config.setMinimumIdle(Integer.parseInt(env.getProperty(MYSQL_DS_PREFIX + i + ".minIdle")));
		config.setMaximumPoolSize(Integer.parseInt(env.getProperty(MYSQL_DS_PREFIX + i + ".maxIdle")));
		config.setConnectionTimeout(Long.valueOf(env.getProperty(MYSQL_DS_PREFIX + i + ".maxWait")));
		config.setConnectionTestQuery(env.getProperty(MYSQL_DS_PREFIX + i + ".validationQuery"));
		config.setDriverClassName("com.mysql.jdbc.Driver");

		HikariDataSource ds = new HikariDataSource(config);
		return ds;
	}

SqlSessionFactoryConfig

Configuration
@AutoConfigureAfter(ShardingDataSourceConfig.class)
public class SqlSessionFactoryConfig {
	private static final Logger log = LoggerFactory.getLogger(SqlSessionFactoryConfig.class);
	/**
	 * 扫描 Mapper 接口并容器管理
	 */
	static final String MAPPER_LOCATION_MYSQL = "classpath*:mapper/mysql/*.xml";
	@Resource(name = "shardingDataSource")
	private DataSource shardingDataSource;

	@Bean(name = "transactionManager")
	// @Primary
	public DataSourceTransactionManager transactionManager() {
		return new DataSourceTransactionManager(shardingDataSource);
	}

	@Bean(name = "mysqlSqlSessionFactory")	
	//@Primary
	public SqlSessionFactory sqlSessionFactory(@Qualifier("shardingDataSource") DataSource shardingDataSource)
			throws Exception {
		final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
		sessionFactory.setDataSource(shardingDataSource);
		sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
				.getResources(SqlSessionFactoryConfig.MAPPER_LOCATION_MYSQL));
		return sessionFactory.getObject();
	}

	@Bean(name = "mysqlSqlSession")
	// @ConditionalOnMissingBean
	public SqlSessionTemplate mysqlSqlSessionTemplate(
			@Qualifier(value = "mysqlSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
	}
	
	/**
	 * 分页插件
	 */
	// @Bean
	// public PageHelper pageHelper(DataSource dataSource) {
	// log.info("注册MyBatis分页插件PageHelper");
	// PageHelper pageHelper = new PageHelper();
	// Properties p = new Properties();
	// p.setProperty("offsetAsPageNum", "true");
	// p.setProperty("rowBoundsWithCount", "true");
	// p.setProperty("reasonable", "true");
	// pageHelper.setProperties(p);
	// return pageHelper;
	// }
}

MyBatis扫描接口MyBatisMapperScannerConfig

@Configuration
@AutoConfigureAfter(SqlSessionFactoryConfig.class)
public class MyBatisMapperScannerConfig {
	private Logger log = LoggerFactory.getLogger(MyBatisMapperScannerConfig.class);

	private String notEmpty = "false";

	private String identity = "MYSQL";

	private String mysqlBasePackage = "com.adou.springboot.mybatis.mapper";

	/**
	 * Mybatis扫描Mysql库相关的SQL文件的配置
	 * 
	 * @return MapperScannerConfigurer对象
	 */
	@Bean(name = "mysqlMapperScanner")
	public MapperScannerConfigurer mysqlMapperScannerConfigurer() {
		log.info("mysql mapper 开始扫描" + mysqlBasePackage);
		MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
		mapperScannerConfigurer.setSqlSessionFactoryBeanName("mysqlSqlSessionFactory");
		mapperScannerConfigurer.setBasePackage(mysqlBasePackage);
		Properties properties = new Properties();
		properties.setProperty("notEmpty", notEmpty);
		properties.setProperty("IDENTITY", identity);
		mapperScannerConfigurer.setProperties(properties);
		log.info("mysql mapper 加载完成");
		return mapperScannerConfigurer;
	}
}

4、测试

PersonController

@RestController
@RequestMapping("/person")
public class PersonController extends BaseController{
	private static final Logger logger = LogManager.getLogger(PersonController.class);

	@Autowired
	private PersonService personService;

	@PostMapping(value = "/selectById")
	public ResponseDTO selectById(String pid) {
		logger.info("====================== pid: " + pid);
		Person  person = personService.selectById(pid);
		ResponseDTO response = onSuccess(person);
		logger.info("====================== response: " + response);
		return response;
	}

}
/**
 * 
 * 控制层基类
 * 
 * @author wangzheng
 * 
 */
public abstract class BaseController {
    /**
     * 日志相关
     */
    protected static Logger LOG = LoggerFactory.getLogger(BaseController.class);

    /**
     * 消息资源对象
     */
    @Autowired
    private MessageSource messageSource;

    /**
     * 成功时返回消息对象。
     * 
     * @param data 业务消息体对象
     * @param T 业务消息体类定义
     * @return 应答结果对象
     */
    public  ResponseDTO onSuccess(T data) {
        ResponseDTO out = new ResponseDTO();

        out.setReturnCode(CommResultCode.SUCCESS);
        out.setReturnMsg(messageSource.getMessage(CommResultCode.SUCCESS, null, LocaleContextHolder.getLocale()));
        out.setResult(data);
        return out;
    }

    /**
     * 业务失败时返回错误失败对象。
     * 
     * @param resultCode 结果码
     * @return 应答结果对象
     */
    public ResponseDTO onFailedBiz(String resultCode) {
        ResponseDTO out = new ResponseDTO();

        out.setReturnCode(resultCode);
        out.setReturnMsg(messageSource.getMessage(resultCode, null, LocaleContextHolder.getLocale()));
        out.setResult(null);
        return out;
    }
    
    /**
     * 业务失败时返回错误失败对象+data
     * 
     * @param resultCode 结果码
     * @param data 错误描述
     * @return 应答结果对象
     */
    
    public ResponseDTO onFailedBiz(String resultCode, String data) {
        ResponseDTO out = new ResponseDTO();

        out.setReturnCode(resultCode);
        out.setReturnMsg(messageSource.getMessage(resultCode, null, LocaleContextHolder.getLocale()));
        out.setResult(data);
        return out;
    }

    /**
     * 业务失败时返回错误失败对象。
     * 
     * @param resultCode 结果码
     * @param msgArgs 返回信息内置参数
     * @return 应答结果对象
     */
    public ResponseDTO onFailedBiz(String resultCode, Object[] msgArgs) {
        ResponseDTO out = new ResponseDTO();

        out.setReturnCode(resultCode);
        out.setReturnMsg(messageSource.getMessage(resultCode, msgArgs, LocaleContextHolder.getLocale()));
        out.setResult(null);
        return out;
    }
    
    /**
     * 时间类型的参数转化绑定
     * @param binder 数据绑定器
     */
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Date.class, new MyDateEditor());
    }

    /**
     * 默认的时间转化类。支持以下格式的字符串转化为Date类。
     * 
  • yyyy-MM-dd HH:mm:ss
  • *
  • yyyy-MM-dd HH:mm
  • *
  • yyyy-MM-dd
  • * * @author wangzheng * */ class MyDateEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = null; if (text == null || text.equals("")) { setValue(date); return; } try { date = format.parse(text); } catch (ParseException e) { format = new SimpleDateFormat("yyyy-MM-dd HH:mm"); try { date = format.parse(text); } catch (ParseException e1) { format = new SimpleDateFormat("yyyy-MM-dd"); try { date = format.parse(text); } catch (ParseException e2) { throw new ParamCheckException("comm.constraints.AssertDate.message", text); } } } setValue(date); } } }

    PersonService

    @Service
    public class PersonService {
    	@Autowired
        private PersonMapper personMapper;
    	
        public Person selectById(String pid) {
            return personMapper.selectPersonById(pid);
        }
        
        
        public int insert(Person person) {
        	return personMapper.insert(person);
        }
    }
    
    

    PersonMapper

    @Mapper
    public interface PersonMapper {
    
        Person selectPersonById(String pid);
    
        List selectAll();
    
        int insert(Person person);
    
        Long update(Person person);
    
        Long delete(Long id);
    }
    
    

    PersonMapper.xml

    
    
    
    
    
        
            INSERT INTO person(id,pid,name,age) VALUES(#{id},#{pid},#{name},#{age})
        
    
        
            UPDATE person SET name=#{name},age=#{age} WHERE pid = #{pid}
        
    
        
            DELETE FROM person WHERE pid = #{pid}
        
    
        
    
        
    
    
    
    

    测试

    SpringBoot-Hikari-Mybatis集成,整合sharding-jdbc,实现分库分表_第2张图片

    四、返回消息设置(国际化)

    MessageConfiguration

    /**
     * @author zhoudoujun01
     * @date:2019年4月24日 下午4:45:08
     * @Description:文本消息配置类。
     */
    @Configuration
    public class MessageConfiguration {
        /**
         * 消息文本资源
         */
        @Autowired
        private MessageSource messageSource;
        /**
         * 消息文本的缓存时间
         */
        @Value("${spring.messages.cache-seconds}")
        private long cacheMillis;
        
        /**
         * 消息文本编码
         */
        @Value("${spring.messages.encoding}")
        private String encoding;
        
        /**
         * 资源文件名称
         */
        private static final String MESSAGES_VALIDATOR = "classpath:messages-validator";
        
        /**
         * 资源文件名称
         */
        private static final String MESSAGES_RESULT = "classpath:messages-result";
    
        /**
         * 配置消息资源
         * @return 消息资源对象
         */
        @Primary
        @Bean
        public MessageSource messageResource() {
            ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
            messageSource.setBasenames(MESSAGES_VALIDATOR, MESSAGES_RESULT);
            messageSource.setDefaultEncoding(encoding);
            messageSource.setCacheMillis(cacheMillis);
            messageSource.setUseCodeAsDefaultMessage(false);
            return messageSource;
        }
        
        /**
         * 设置默认区域解析器(中国)
         * @return 区域解析器
         */
        @Bean
        public LocaleResolver localeResolver() {
            FixedLocaleResolver slr = new FixedLocaleResolver();
            // 默认区域
            slr.setDefaultLocale(Locale.CHINA);
            return slr;
        }
        
        /**
         * 取得消息国际化访问器
         * 
         * @return 消息国际化访问器
         */
        @Bean
        public MessageSourceAccessor messageSourceAccessor() {
    //        LocaleContextHolder.setDefaultLocale(Locale.US);
            return new MessageSourceAccessor(messageSource, LocaleContextHolder.getLocale());
        }   
    }
    

    messages-result_zh_CN.properties

    0000 = 成功
    0001 = 服务器内部错误
    0002 = 请求参数非法,错误信息为:{0}
    0003 = ak不存在或者非法
    

    五、SpringBoot-Hikari-Mybatis集成Demo

    springboot-mybatis

    你可能感兴趣的:(springboot,springboot,hikari,sharding-jdbc,分库,分表)