对 于一个刚上线的互联网项目来说,由于前期活跃用户数量并不多,并发量也相对较小,所以此时企业一般都会选择将所有数据存放在一个数据库中进行访问操作。但 随着后续的市场推广力度不断加强,用户数量和并发量不断上升,这时如果仅靠一个数据库来支撑所有访问压力,几乎是在自寻死路。所以一旦到了这个阶段,大部 分Mysql DBA就会将数据库设置成读写分离状态,也就是一个Master节点对应多个Salve节点。经过Master/Salve模式的设计后,完全可以应付单 一数据库无法承受的负载压力,并将访问操作分摊至多个Salve节点上,实现真正意义上的读写分离。但大家有没有想过,单一的Master/Salve模 式又能抗得了多久呢?如果用户数量和并发量出现量级上升,单一的Master/Salve模式照样抗不了多久,毕竟一个Master节点的负载还是相对比 较高的。为了解决这个难题,Mysql DBA会在单一的Master/Salve模式的基础之上进行数据库的垂直分区(分库)。所谓垂直分区指的是可以根据业务自身的不同,将原本冗余在一个数 据库内的业务表拆散,将数据分别存储在不同的数据库中,同时仍然保持Master/Salve模式。经过垂直分区后的Master/Salve模式完全可 以承受住难以想象的高并发访问操作,但是否可以永远高枕无忧了?答案是否定的,一旦业务表中的数据量大了,从维护和性能角度来看,无论是任何的CRUD操 作,对于数据库而言都是一件极其耗费资源的事情。即便设置了索引,仍然无法掩盖因为数据量过大从而导致的数据库性能下降的事实,因此这个时候Mysql DBA或许就该对数据库进行水平分区(分表,sharding),所谓水平分区指的是将一个业务表拆分成多个子表,比如user_table0、 user_table1、user_table2。子表之间通过某种契约关联在一起,每一张子表均按段位进行数据存储,比如user_table0存储 1-10000的数据,而user_table1存储10001-20000的数据,最后user_table3存储20001-30000的数据。经过 水平分区设置后的业务表,必然能够将原本一张表维护的海量数据分配给N个子表进行存储和维护,这样的设计在国内一流的互联网企业比较常见,如图所示:
水平分区
以上是数据库分库分表的原理,但是如果每次在数据访问层的设计中实现分库分表,将会显得非常麻烦。笔者参考了了下code google shardbatis的设计方式,发现googlecode上的shardbatis无法做到分库,只能做到分表,而且配置相当不灵活,源码很难找到。所 以笔者重新设计了一套mybatis插件,相比google code的那套,该框架可以做到完整的分库分表和友好的配置。组件结构如图所示:
组件结构图
使用方法:
1.准备数据库集群配置文件
Xml代码
<?xml version="1.0" encoding="UTF-8"?> <databases xmlns="http://fangjialong.iteye/schema/snsprod" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.vivo.com.cn/schema/snsprod http://fangjialong.iteye/schema/snsprod http://fangjialong.iteye/schema/snsprod/shardbatis-db.xsd"> <!-- 全局配置 --> <logicName value="test"/> <configs> <property name="minPoolSize" value="4" /> <property name="minPoolSize" value="8" /> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="maxIdleTime" value="900" /> <property name="idleConnectionTestPeriod" value="1800" /> </configs> <database suffix="_00" username="root" password="" jdbcUrl="jdbc:mysql://localhost:3306/test_00?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull"> <property name="minPoolSize" value="4" /> <property name="minPoolSize" value="8" /> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="maxIdleTime" value="900" /> <property name="idleConnectionTestPeriod" value="1800" /> </database> <database suffix="_01" username="root" password="" jdbcUrl="jdbc:mysql://localhost:3306/test_01?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" /> </databases>
2.准备分库分表规则配置
Xml代码
<configs xmlns="http://fangjialong.iteye.com/schema/snsprod" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://fangjialong.iteye.com/schema/snsprod http://fangjialong.iteye.com/schema/snsprod http://fangjialong.iteye.com/schema/snsprod/shardbatis-config.xsd"> <strategy logicTable="t_student" class="com.cannon.prod.dal.strategy.StudentShardStrategy"/> </configs>
3.为mybatis配置扩展插件
Xml代码
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <typeAliases> <typeAlias alias="Student" type="com.cannon.prod.dal.model.Student" /> </typeAliases> <plugins> <plugin interceptor="com.cannon.prod.dal.shardbatis.ShardPlugin"> <property name="configsLocation" value="META-INF/mybatis/shardbatis-config.xml" /> </plugin> </plugins> <mappers> <mapper resource="META-INF/mybatis/mapper/mybatis-mapper-student.xml" /> </mappers> </configuration>
4.为mybatis配置分库分表数据源
Xml代码
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd"> <!-- 配置数据源 --> <bean id="distributeDefaultDataSource" class="com.cannon.prod.dal.shardbatis.ShardDataSource" init-method="init" destroy-method="destroy"> <property name="configsLocation" value="META-INF/database/distributed-default-db.xml"></property> </bean> <bean id="distributeDefaultTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="distributeDefaultDataSource" /> </bean> <bean id="distributeDefaultTransactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="distributeDefaultTransactionManager" /> </bean> <bean id="distributeDefaultSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation"> <bean class="org.springframework.core.io.ClassPathResource"> <constructor-arg index="0" value="META-INF/mybatis/mybatis-config.xml" /> </bean> </property> <property name="dataSource" ref="distributeDefaultDataSource" /> </bean> </beans>
5.使用逻辑表配置mybatis 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.cannon.prod.dal.mapper.Student"> <resultMap type="Student" id="StudentResultMap"> <id property="no" column="no" /> <result property="name" column="name" /> <result property="sex" column="sex" /> </resultMap> <!-- 查询学生,根据id --> <select id="getByNo" parameterType="String" resultType="Student" resultMap="StudentResultMap"> <![CDATA[ SELECT * FROM `t_student` WHERE `no` = #{no} ]]> </select> <delete id="deleteByNo" parameterType="String"> <![CDATA[ DELETE FROM `t_student` WHERE `no` = #{no} ]]> </delete> <insert id="create" parameterType="Student"> INSERT INTO t_student(no, name, sex) VALUES(#{no}, #{name}, #{sex}) </insert> <update id="update" parameterType="Student"> UPDATE t_student <trim prefix="SET" suffixOverrides=","> <if test="name != null">name=#{name},</if> </trim> WHERE no=#{no} </update> </mapper>
6.编写分库分表规则
Java代码
public class StudentShardStrategy implements ShardStrategy { @Override public ShardCondition parse(Map<String, Object> params) { String no = (String) params.get("no"); char c = no.charAt(0); ShardCondition condition = new ShardCondition(); if (c == '1') { condition.setDatabaseSuffix("_01"); condition.setTableSuffix("_01"); } else { condition.setDatabaseSuffix("_00"); condition.setTableSuffix("_00"); } return condition; } }
全部配置好了之后就可以按照以前使用mybatis的方式将数据库集群当做一个逻辑表来操作了。剩余的转换操作就交给中间扩展层插件来转换吧。
以下是转换结果:
Shard Original SQL:SELECT * FROM `t_student` WHERE `no` = ?
Shard Convert SQL:SELECT * FROM `test_00`.`t_student_00` WHERE `no` = ?
Shard Original SQL:DELETE FROM `t_student` WHERE `no` = ?
Shard Convert SQL:DELETE FROM `test_00`.`t_student_00` WHERE `no` = ?
Shard Original SQL:INSERT INTO t_student(no, name, sex) VALUES(?, ?, ?)
Shard Convert SQL:INSERT INTO test_00.t_student_00 (no, name, sex) VALUES (?, ?, ?)
Shard Original SQL:UPDATE t_student SET name=? WHERE no=?
Shard Convert SQL:UPDATE test_00.t_student_00 SET name = ? WHERE no = ?
注意:分库分表从原则上是不支持跨库事物的,如果需要使用事务必须保证在多个表在同一个库中。
以下是事物支持的测试用例:
Java代码
/** * @author fangjialong * @date 2015年9月5日 下午4:27:50 */ public class StudentDAOTransactionTest { private DalSpringContext context; private StudentDAO studentDAO; private TransactionTemplate tt; @Test public void test() { LogFactory.useNoLogging(); this.context = new DalSpringContext(); this.context.refresh(); this.studentDAO = context.getBean(StudentDAO.class); this.tt = context.getBean("distributeDefaultTransactionTemplate", TransactionTemplate.class); tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { Student s = new Student(); s.setNo("0001"); s.setName("房佳龙"); studentDAO.update(s); status.setRollbackOnly(); } }); context.close(); } }