對 於一個剛上線的互聯網項目來講,因爲前期活躍用戶數量並很少,併發量也相對較小,因此此時企業通常都會選擇將全部數據存放在一個數據庫中進行訪問操做。但 隨着後續的市場推廣力度不斷增強,用戶數量和併發量不斷上升,這時若是僅靠一個數據庫來支撐全部訪問壓力,幾乎是在自尋死路。因此一旦到了這個階段,大部 分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_table一、user_table2。子表之間經過某種契約關聯在一塊兒,每一張子表均按段位進行數據存儲,好比user_table0存儲 1-10000的數據,而user_table1存儲10001-20000的數據,最後user_table3存儲20001-30000的數據。通過 水平分區設置後的業務表,必然可以將本來一張表維護的海量數據分配給N個子表進行存儲和維護,這樣的設計在國內一流的互聯網企業比較常見,如圖所示:
java
水平分區mysql
以上是數據庫分庫分表的原理,可是若是每次在數據訪問層的設計中實現分庫分表,將會顯得很是麻煩。筆者參考了了下code google shardbatis的設計方式,發現googlecode上的shardbatis沒法作到分庫,只能作到分表,並且配置至關不靈活,源碼很難找到。所 以筆者從新設計了一套mybatis插件,相比google code的那套,該框架能夠作到完整的分庫分表和友好的配置。組件結構如圖所示:spring
組件結構圖sql
使用方法:數據庫
1.準備數據庫集羣配置文件mybatis
<?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.準備分庫分表規則配置app
<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配置擴展插件ide
<?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 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 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.編寫分庫分表規則
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 = ?
注意:分庫分表從原則上是不支持跨庫事物的,若是須要使用事務必須保證在多個表在同一個庫中。
如下是事物支持的測試用例:
/** * @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(); } }