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