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、架构图

在这里插入图片描述

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、数据库分库策略

/**
 * 分库算法:编号后两位</br>
 * 
 * @author zhoudoujun01
 * @date 2019年8月6日14:55:06
 */

public class DatabaseShardingAlgorithm implements ComplexKeysShardingAlgorithm {
	public Collection<String> doSharding(Collection<String> availableTargetNames, Collection<ShardingValue> shardingValues) {
		// 根据分片键名取得分片值列表
		Collection<String> orderIdValues = getShardingValue(shardingValues, Constant.COL_NAME_ID);
		
		//
		Collection<String> 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<String> 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<String> getShardingValue(Collection<ShardingValue> shardingValues, final String key) {
		Collection<String> valueSet = new ArrayList();
		Iterator<ShardingValue> iterator = shardingValues.iterator();
		while (iterator.hasNext()) {
			ShardingValue next = iterator.next();
			if (next instanceof ListShardingValue) {
				@SuppressWarnings("unchecked")
				ListShardingValue<String> value = (ListShardingValue<String>) 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<String, DataSource> createDataSourceMap() {
		HashMap<String, DataSource> 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 <T> ResponseDTO<T> onSuccess(T data) {
        ResponseDTO<T> out = new ResponseDTO<T>();

        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<String> 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类。
     * <li>yyyy-MM-dd HH:mm:ss</li>
     * <li>yyyy-MM-dd HH:mm</li>
     * <li>yyyy-MM-dd</li>
     * 
     * @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<Person> selectAll();

    int insert(Person person);

    Long update(Person person);

    Long delete(Long id);
}

PersonMapper.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.adou.springboot.mybatis.mapper.PersonMapper">

    <insert id="insert" parameterType="com.adou.springboot.mybatis.model.Person">
        INSERT INTO person(id,pid,name,age) VALUES(#{id},#{pid},#{name},#{age})
    </insert>

    <update id="update" parameterType="com.adou.springboot.mybatis.model.Person">
        UPDATE person SET name=#{name},age=#{age} WHERE pid = #{pid}
    </update>

    <delete id="delete" parameterType="java.lang.Long">
        DELETE FROM person WHERE pid = #{pid}
    </delete>

    <select id="selectPersonById" resultType="com.adou.springboot.mybatis.model.Person">
        select * from person where pid = #{pid}
    </select>

    <select id="selectAll" resultType="com.adou.springboot.mybatis.model.Person">
        SELECT id, pid,name,age FROM person
    </select>


</mapper>
测试

在这里插入图片描述

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

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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值