2018年01月31日
隨着我們系統用戶數量的日增,業務數據處於一個爆發前,增長的數據量已經給我們的系統造成了很大的不確定。在上個周末用戶量較多,並發較大的情況下,讀寫頻繁的驗證碼表,數據量達到幾十萬上百萬的時候出現了鎖表阻塞,導致用戶登錄驗證失敗,進而導致系統的一度反應較慢,甚至登錄不上等問題。查了很多資料,發現大家都是偏理論,索性自己實現了,發出來以作記錄,也能給別人一些幫助。諸位有什么高明意見,歡迎交流。個人QQ:1612301243,非誠勿擾。
由於這種讀寫更新頻繁的表,造成性能下降,僅僅通過添加redis緩存已經解決不了問題。所以我們引入了數據庫表切分的解決方案。
考慮到業務表數據量的增長情況,我們決定采用按周或者按月的方式進行表的切分,具體路由規則如下:
切分策略,見仁見智。
package
com.**.uc.utils;
import
java.util.Calendar;
import
org.apache.commons.lang.StringUtils;
public
class
TableRouter {
/**
* table路由規則,獲取新表名稱
*
@param
prefix 表明前綴
*
@param
strategy 切分策略,
*
@return
*/
public
static
String getUcCaptchaTable(String prefix,String strategy ){
//根據切分策略進行切分,添加一定的容錯,該部分主要是針對讀寫頻繁的驗證碼表,故部分代碼寫死為主表的數據;
//切分策略為周時,返回“表名_年份周次”,也就是說一年會有52張表
//切分策略為月時,返回“表名_年份月份”,也就是說一年會有12張表
//該種切分策略的弊端,是在周末凌晨或者月末凌晨的幾分鍾,存在驗證不存在的情況,在我們的系統允許范圍內,故此處未做特殊處理。
if
(StringUtils.isNotBlank(prefix)&&StringUtils.isNotBlank(strategy)&&prefix.equals(
"uc_captcha"
)&&
"week"
.equals(strategy)){
Calendar c=Calendar.getInstance();
int
i = c.get(Calendar.
WEEK_OF_YEAR
);
StringBuffer sb =
new
StringBuffer();
int
year = c.get(Calendar.
YEAR
);
String suffix = sb.append(year).append(i).toString();
System.
out
.println(suffix);
return
prefix+
"_"
+suffix;
}
else
if
(StringUtils.isNotBlank(prefix)&&StringUtils.isNotBlank(strategy)&&prefix.equals(
"uc_captcha"
)&&
"month"
.equals(strategy)){
Calendar c=Calendar.getInstance();
int
i = c.get(Calendar.
MONTH
);
StringBuffer sb =
new
StringBuffer();
int
year = c.get(Calendar.
YEAR
);
String suffix = sb.append(year).append(i).toString();
System.
out
.println(suffix);
return
prefix+
"_"
+suffix;
}
//獲取不到分表名稱,則返回主表名稱
return
"uc_captcha"
;
}
}
|
切分策略寫好后,關鍵的是我們需要將我們的sql中對應的表名更改為動態傳入,此處用到的是mybatis的多參數映射屬性。
部分java代碼與xml代碼如下:
java代碼如下(支持集中基本的數據類型,注意map的寫法,list的話,采用list 小寫):
/**
插入一條數據
**/
public
int
add(
@Param
(
"table"
) String table ,
@Param
(
"map"
) Map<String,Object> map);
/**
更新一條數據
**/
public
int
update(
@Param
(
"table"
) String table ,
@Param
(
"map"
) Map<String,Object> map);
|
xml文件:注意表名的寫法 ${table}使用${}不攜帶jabcType,也不能使用#;map取參數,使用map.prapmeter 取參數
<!-- 插入一條新記錄 -->
<
insert
id
=
"add"
parameterType
=
"map"
>
insert into ${table}(pid,btype,uid,naccount,capthcha,ntype,ctime,expiration)
values(
#{map.pid, jdbcType=VARCHAR},
#{map.type, jdbcType=VARCHAR},
#{map.uid, jdbcType=VARCHAR},
#{map.phone, jdbcType=VARCHAR},
#{map.code, jdbcType=VARCHAR},
#{map.is_active, jdbcType=VARCHAR},
#{map.ctime, jdbcType=VARCHAR},
#{map.invalid_time, jdbcType=VARCHAR}
)
<
selectKey
resultType
=
"int"
keyProperty
=
"pid"
>
SELECT @@IDENTITY AS pid
</
selectKey
>
</
insert
>
|
domain層調用如下:
int
validation_id =
validationDao
.add(getCurrentTableName(),map);
|
其中getCurrentTableName()為內部方法,其中是根據配置的策略以及路由規則獲取分表表名,代碼如下:
/**
* 獲取當前分表名稱
*/
public String getCurrentTableName() {
String tableName = TableRouter.
getUcCaptchaTable("uc_captcha", strategy);
if(!
this._this.existTable(tableName)){//不存在新表,則創建新表,並返回新表表名
try {
int tableCreateRes = validationDao.dynamicCreateTable(tableName);
if(tableCreateRes >=0){
//創建新表,清空表不存在的緩存,
this._this.notExistTable(tableName);
}
}
catch (Exception e) {
return "uc_captcha";
}
}
return tableName;
}
/**
* 緩存表是否存在,減輕
*/
@Cacheable
(value=
"uc2cache"
, key=
"'uc_captcha_exist_'+#tableName"
)
public
boolean
existTable(String tableName){
int
tableCount =
validationDao
.existTable(tableName);
if
(tableCount == 0){
//不存在新表,則創建新表,並返回新表表名
return
false
;
}
return
true
;
//存在
}
@CacheEvict
(value=
"uc2cache"
, key=
"'uc_captcha_exist_'+#tableName"
)
public
void
notExistTable(String tableName){}
|
考慮到每次都會調用數據庫查詢表是否存在,我們為減少對數據庫的IO,我們采用了redis緩存的方式,其中AOP切面,自調用不起作用的情況,不在此處贅述。
你可以看到,不存在路由的分表的時候,我們會進行創建表,創建語句如下:
<!-- 查詢表是否存在 -->
<
select
id
=
"existTable"
parameterType
=
"String"
resultType
=
"Integer"
>
select count(1)
from information_schema.tables
where LCASE(table_name)=#{table,jdbcType=VARCHAR}
</
select
>
<!-- 創建表 -->
<
update
id
=
"dynamicCreateTable"
parameterType
=
"String"
>
CREATE TABLE if not EXISTs ${table} (
`pid` varchar(36) NOT NULL,
`uid` int(11) DEFAULT NULL,
`btype` varchar(30) NOT NULL COMMENT '業務類型例如:sign 用戶注冊。login 用戶登陸',
`ntype` varchar(30) NOT NULL COMMENT '短信、郵箱、微信等。根據系統支持取值',
`naccount` varchar(30) NOT NULL COMMENT '手機號、郵箱、微信等',
`capthcha` varchar(6) NOT NULL COMMENT '6位隨機驗證碼',
`expiration` int(11) NOT NULL COMMENT '有效期,距離1970年秒數',
`ctime` int(11) NOT NULL COMMENT '創建時間距離1970年秒數',
PRIMARY KEY (`pid`),
KEY `fk_uccaptcha_uid` (`uid`),
KEY `uk_uc_captcha_ub` (`btype`) USING BTREE,
CONSTRAINT ${table}_ibfk_1 FOREIGN KEY (`uid`) REFERENCES `uc_users_ext` (`uid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
</
update
>
<!--成功返回0 失敗會跑錯,我們已經做了容錯處理-->
|
至此,數據庫的切分表功能基本完成。
發一個beanselfaware的鏈接 :
http://fyting.iteye.com/blog/109236