该文章来自于阿里巴巴技术协会(ATA)精选文章。
一、总体
在阿里通信必零(计费重构)项目中,在完成项目建设的同时,我们把日常常用的一些工具类进行了抽取沉淀,形成了基础库。我觉得有些东西挺好用的,写篇文章介绍一下。 二方库:
<dependency>
<groupId>com.alicom</groupId>
<artifactId>alicom-frame</artifactId>
<version>1.0.7</version>
</dependency>
主要内容包括:
1、 DAO处理,灵活方便的分库分表策略,可以支持自由自定义分库分表规则
2、 通过注解,实现方法级缓存,通过dimaond配置控制缓存更新
3、 利用tair实现分布式锁
4、 jmx支持,通过注解方便的jmx支持
5、 通信客户端,包括:http/https,ftp/ftps,sftp,oss
6、 其它一些工具类,主要包括:
a、FrameDiamondUtil:Diamond配置读取,自动监听diamond的配置变更,保存在内部的CACHE,不需要调用者写Listener
b、FrameBeanFactory:便捷的获取spring bean
c、FrameTimeUtil:便捷的时间处理函数,线程安全,基于joda封装的时间处理类,性能快
d、JacksonUtil:使用jackson进行json处理;EncryptUtil:3des加减密;Md5Util:md5摘要生成
接下去就其中几块内容展开说明一下。
二、DAO操作,方便的分库分表支持
1、先来说一下规规矩矩的spring-ibatis如何实现DAO操作
a、首先是datasource配置,这个是必须得,也少不了。我们一般都是用TDDL的Group层,在这配置对应数据源的appName和dbGroupKey。有几个数据源就配置几个bean
b、编写我们的sqlmap文件,xxxxx1_sqlmap.xml,xxxxx2_sqlmap.xml,一个表一个sqlmap,在sqlMapConfig中,对sqlmap文件进行引用
c、配置sqlMap操作类sqlMapClient或sqlMapClientTemplate,需要配置sqlMapConfig和DataSources属性,一个DataSource需要配一个sqlMapClient
d、编写DAO类,根据不同的操作,sqlMapClient或sqlMapClientTemplate执行对应的sqlmap语句;
如果涉及分库分表,就需要做些加工。分表意味着sqlmap中对应的表名是由外部参数传递构建的,我们一般会写TableRoute之类的东西,根据规则拼接表名;分库本质上就使用不同DataSource的sqlMapClient.,我们一般会写DbRoute之类的东西,根据规则定位sqlMapClient。
当然涉及事务,还需要增加一些事务相关的配置处理。传统的方式,问题主要有:
• 配置多而繁琐
• 分库分表规则的支持上不灵活,分库分表对DAO代码的侵入很大
2、我们提供了什么
a、配置文件
•persistence.xml:数据源datasource相关的配置还是少不了的,有关sequence,事务的配置原来该怎么样还是怎么样。
但不再需要手动配置sqlMapClient或SqlMapClientTemplate,只需要设置DAO路径和数据源映射关系,对于分库只要加通配DBNAME就可以了。
•sql-map-config.xml和sql-map-null.xml文件(基本不用改,所有项目都一样)
一个是 sqlMapConfig全局配置,一个是只有schema的文件,用于内部构造作为模板。
b、命名和目录约定
所有DAO文件在dao目录下,所有DO文件在dataobject目录下,目录平行;
DAO实现类以xxxx DAOImpl命名,对应的DO类是xxxxDO。
为什么有这个约定,后续会有解释。
c、DAO实现类继承BaseDAO,通过注解指定sqlMap文件,自动注册为bean。
d、DO实现继承BaseDO,通过@DbDefine 指定分表规则,通过@TableDefine指定分表规则。
我们一般使用代码生成工具构建DAO/DO、SQLMAP代码,这些都全自动化了。
e、对于分库分表特别介绍一下
•sqlmap文件简单调整,需要将表名变更为_TABLE_NAME_ ,如:
•在DO类上加注解@TableDefine
[]里的内容可以约定哪个字段作为分表key,如何进行key转换。如上面就是 以gmt_create作为分表键,分表转换规则见monthTableConvert类
另外可以用多个[]号,指定更为复杂的分表规则,如: @TableDefine("zw_add_month_[monthTableConvertByBillMonth]_[acctIdTableConvert]")
•如果不涉及分库,通过前面说的DAO路径和DataSource映射就可以获取最终DS了。
•如果涉及分库,根据映射规则拿到的是带通配符DBNAME的DS名。需要在DO类上加注解@DbDefine,用法同@ TableDefine,通过注解指定分库key和分库转换方法类,最终替换通配符,获取真正DS.
•DAO实现方法,可以通过DO、Query、Map对象向最终的sqlmap传递参数,如果涉及分库分表,对象中一定要包含分表key字段。
3、如何实现
•AbstractConvert
所有分库分表规则的抽象类,具体分库分表规则可以通过实现这个类来提供。
如:按ID取模分64个表、按ID取模分4个库、按时间戳按月分表、按时间戳按日分表、按时间戳按年分表等等。
提供了两个方法需要实现:
String getDefaultKey() 如果对外注解没指定分库或分表key,就缺省用这个key。
String convert(String key,Object) 具体如何转换分库或分表逻辑。
•BaseDO
主要提供了set_TABLE_NAME_(),供分表使用。
•BaseDAO
所有DAO操作基类
初始化的时候:
根据@SQLMap注解,获取sqlmap路径;
找到同级的DO(前面约定的DAO和DO命名规范),根据是否有@DbDefine注解,判断是否分库和分库规则,根据是否有@TableDefine注解,判断是否分表和分表规则;
sql操作的时候,根据getTemplate方法,获取SqlMapClientTemplate。
主要操作包括:如果非分库,直接根据包路径映射获取ds名,否则根据分库规则替换通配符,获取ds,根据ds构建数据源(第一次是构建,后续会从Map中取);如果是分表的,根据分表规则获取最终表名,最终通过操作对象传递到sqlmap。
三、简单易用的Diamond工具类FrameDiamondUtil
1、Diamond的普通使用方式
Diamond真是个好东西,阿里内部很多动态配置的业务场景都是由Diamond实现的。有关Diamond的介绍,大家可以自己搜。
Diamond使用主要两个方法
•Diamond.getConfig(dataId,group,timeoutMs)
•Diamond. addListener(dataId,group,new ManagerListenerAdapter()
{ public void receiveConfigInfo(String configInfo) { …}
}
为此我们一般都需要在使用Diamond的类里加init方法,调用getConfig获取相应配置,并且赋值给我们的业务变量;同时增加一个监听器,实现receiveConfigInfo方法,当diamond配置变更的时候替换我们得业务变量。
2、我们提供了什么
我们觉得这样使用,每个用到Diamond的类都需要添加类似代码,似乎不太优雅。能不能封装一个工具类,直接通过静态方法就可以获取Diamond配置值,而有关Diamond推送变更相关代码也隐式的做到,对调用者透明呢?
FrameDiamondUtil就是这样一个工具类。提供了
public static String getConfig(String dataId,String group,long timeoutMs)静态方法,外部使用者只需要调用这个静态方法简单赋值,其它都不用管。
如果希望返回的不是原生配置的String内容 ,而是自己做些转换,可以调用
public static Object getConfig(String dataId,String group,long timeoutMs,Convert convert),自己实现Convert类就可以。
3、如何实现
其实也挺简单的,内置一个Map
调用getConfig的时候,以group^dataId作key,从Map中取。如果是第一次调用,调用Diamond.getConfig获取,以及注册监听器。
在监听器代码中,对Map内容进行替换
四、通过注解,实现方法级缓存
1、解决什么问题
利用AOP,将缓存实现和业务逻辑分离,相信大家都有所耳闻。比如采用SpringCache框架,通过注解就能实现方法级缓存。
有关SpringCache的介绍,可以见 http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/ 我们这套框架就是基于spring Cache做了些扩展,配合Diamond可以定时更新缓存而无需重启。
2、我们提供了什么
通过spring的注解实现方法级缓存 ;支持缓存到本地JVM或者Tair中,本地缓存遵循LRU策略;通过diamond对缓存生命周期进行管理
使用举例:
a、声明注解
b、配置文件,指定需要管理的Diamond及cacheManager配置
c、只要在方法上加注解,就可以实现缓存功能
d、dimaond管理缓存生命周期
当时间大于配置内容,就刷新缓存(其实是替换缓存的key),但外部感觉就像缓存刷新
3、如何实现
a、整体上依赖spring Cache
b、实现了spring Cache的Cache接口
本地缓存使用google的concurrentlinkedhashmap,内存超出的时候使用LRU;
通过配置确定是否使用tair,如果配置了enableTair,本地读取不到的时候读tair,put的时候同时put本地和tair。
c、对SpringCache的KeyGenerator进行了特殊实现
一般大家用SpringCache的时候都是采用DefaultKeyGenerator,通过方法名和参数名构建Key.(当然也有通过表达式自定义key的)。
我们对KeyGenerator做了特殊实现,key的组成除了方法名和参数名外,将从Diamond相关配置中读取的时间戳作为key组成部分的前缀,如果Diamond内容发生了变更,相应的key就变化,这个时候根据改key就取不到缓存,会触发真正的方法调用,等到第二次调用的时候缓存key没有变化,那就能读到缓存。
五、 分布式锁
1、说明
基于tair的原子操作实现的分布式锁。
2、使用方式
a、实现TairLockObject
举例CreditAcctIdLock,确定lock key的前缀KEY_PREFIX,确定配置项CONFIG_PREFIX;构造函数中传入acctId
b、使用锁
构建对象 AcctIdLock lock = new AcctIdLock(6536182776198L) ;
获取锁 lock.acquire();
释放锁 lock.release()。
3、怎么实现
相对比较简单,依赖tair的原子操作。
TairLockObject决定了key的组成,决定了如何获取tair配置(如:数据库或配置文件),此外还包括获取锁的一些策略:如获取锁失败的重试次数和等待时间。
加锁:基于 key做 tairManager.incr
释放锁:基于key做tairManager.delete
六、总结
唠唠叨叨讲了一些,该收场了。个人觉得在项目实施的过程中,除了完成特定业务目标外,还能逐步沉淀出一些技术框架,供后续工程使用,避免重复造轮子,还是挺有意义的。