2017 年记录
php转java小结
经过2个多月的奋战,运营及电催系统php转java终于上线。为开发的过程做个小结。
虽然大多java开发不太懂php,好在语言这个东西虽然大家不会使用php来做开发,但是基本上能看懂php代码的逻辑。
不吐槽php代码里面的一些神奇的逻辑,针对于此次重构只从技术上做写总结。
一、Java8特性
在我们的代码中,有大量的List
stream结合lambda让这些转变变成so easy。
List->List
List<Case> caseList = caseMapper.selectByExample(caseExample);
List<Integer> companyIdList = caseList.stream()
.map(Case::getClientCompanyId)
.collect(Collectors.toList());
List<Integer> companyIdList1 = caseList.stream()
.map(caze->{
if(caze.getClientCompanyId()==0){
return -1;
}
return caze.getClientCompanyId()+0;
})
.collect(Collectors.toList());
过滤list
/**
* 过滤null
*/
relationshipTargetList = relationshipTargetList.stream().filter(r -> r!=null).collect(Collectors.toList());
List->Map
List<Debtor> debtorList = debtorMapper.selectByExample(debtorExample);
Map<Integer, Debtor> debtorMap = debtorList.stream().collect(Collectors.toMap(Debtor::getCaseId, d -> d,
(key1, key2) -> key1));
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
Map.getOrDefault()
再也不用担心空指针了
/**
* # 案件发布公司
*/
CompanyInfo companyInfo = companyInfoMap.getOrDefault(caze.getClientCompanyId(),defaultCompanyInfo);
result.setClient_company(companyInfo.getName());
二、Apache common包的使用
StringUtils绝对是字符串操作利器,大部分对字符串的常用操作都有实现,不用担心空指针。推荐指数五星级。
NumberUtils数字转换利器,不管你是toInt、toLong还是toDouble,使用它保证你不会后悔。
三、ElasticSearch
此次我们重构直接把版本升级至5.1.1,调用方式由原php的restful形式改成client的api接口形式的调用。
今天不解释分词、不介绍打分,只说我们项目中使用到的干货。
安装
这里有个简单的安装总结,大家参考一下
http://note.youdao.com/noteshare?id=b7ad559c78872fda66c53a39996e337f
使用需求
类比mysql,我们需要=、>、>=、<、<= 、or、and、in、like、count、sum、group by、order by、limit
在高版本的ES里面使用了boolquery替换了filter
等于号=
QueryBuilders.termQuery("isDel", 1))
// termQuery相当于=,相当于isDel=1
大于小于号
QueryBuilders.rangeQuery("overdueStarttime").lte(startDate.getTime())
//rangeQuery
//lte <=
//lt <
//gte >=
//gt >
or、and
boolQueryBuilder.must(QueryBuilders.termQuery("isDel", req.getIs_del()));
//must相当于and
caseStatusQuery.should(QueryBuilders.termQuery("caseStatus", s));
//should 相当于or
in
QueryBuilders.termsQuery("clientCompanyId",req.getClientCompanyIdList())
//termsQuery 相当于in,可以传递list
like
QueryBuilders.wildcardQuery("phone1", "*" + req.getTelephone() + "*")
//wildcard 正则匹配
QueryBuilders.matchPhraseQuery("debtorName", req.getDebtor_name())
//matchPhrase 不会分词,直接匹配有完整相关短语的记录
sum && count
AggregationBuilder debtTotalSum = AggregationBuilders.sum(DEBT_TOTAL_SUM_KEY).field("debtTotal");
AggregationBuilder debtTotalCount = AggregationBuilders.count(DEBT_TOTAL_COUNT_KEY).field("debtTotal");
group by
AggregationBuilder clientCompanyIdAgg = AggregationBuilders.terms("clientCompanyIds").field("clientCompanyId").size(5000);
//以clientCompanyId字段聚合,别名为clientCompanyIds
order by and limit
SearchResponse searchResponse = client.prepareSearch(ElasticSearchUtil.getIndexName())
.setTypes(ElasticSearchUtil.TYPE_NAME)
.setQuery(boolQueryBuilder)
//指定查询字段
.addStoredField("id")
.addSort(order, sortOrder)
//分页
.setFrom((currentPage - 1) * limit).setSize(limit)
.execute()
.actionGet();
批量插入
public void batchInsert(List<CaseTable> list,Client client) {
LOGGER.info("---------案件批量插入索引开始--------");
BulkRequestBuilder bulkRequest = client.prepareBulk();
long start = System.currentTimeMillis();
BulkRequestBuilder delRequest = client.prepareBulk();
for (CaseTable caseTable :list){
IndexRequestBuilder ir = client.prepareIndex()
.setIndex(ElasticSearchUtil.INDEX_NAME_CASE)
.setType(ElasticSearchUtil.TYPE_NAME_CASE)
.setId(caseTable.getId().toString())
.setSource(JSON.toJSONString(caseTable));
bulkRequest.add(ir);
start = System.currentTimeMillis();
BulkResponse bulkResponse = bulkRequest.execute().actionGet();
LOGGER.info("案件批量插入循环所用时间 search time:{}ms", System.currentTimeMillis() - start);
if (!bulkResponse.hasFailures()) {
LOGGER.info("----------案件批量插入成功一轮------------");
}
}
踩过的坑
- 当查询条件多余1024个时,会报错
- 当分页的记录大于10000时,会报错
- 当使用浮点数进行sum时会有精度丢失
- 当group by字段是非数字时,会报错
- 如果terms(in)条件比较多的话,使用must则非常慢
- 大数值的比较不生效
如1505446228090(13位)居然小于905446228090(12位)
- ES的数据存放目录被设置到了系统盘,导致磁盘被沾满
四、数据库的读写分离
实现spring封装的AbstractRoutingDataSource,很容易在多数据源之间进行切换
AbstractRoutingDataSource 一些关键代码
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if(dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if(dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
流程
当调用getConnection()方法获取connection连接时,AbstractRoutingDataSource 会先执行determineTargetDataSource()方法来找到当前的数据源。
先根据lookupKey从resolvedDataSources(Map
动态数据源的一个实现
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<DatabaseType>();
public static void setDataSourceType(DatabaseType databaseType) {
contextHolder.set(databaseType);
}
public static DatabaseType getDataSourceType() {
return contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceType();
}
}
DynamicDataSource的使用
/**
* 数据源设置
*/
@Bean
public DynamicDataSource dataSource() throws SQLException {
DruidDataSource primaryDataSource = primaryDataSource();
Map<Object, Object> mapDataSources = new HashMap<Object, Object>();
mapDataSources.put(DatabaseType.zichan360_case, primaryDataSource);
mapDataSources.put(DatabaseType.zichan360_case_read,primaryDataSourceRead());
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(mapDataSources);// 该方法是AbstractRoutingDataSource的方法
dataSource.setDefaultTargetDataSource(primaryDataSource);// 默认的datasource设置为primaryDataSource
return dataSource;
}
怎么切换读写数据源呢
- AOP根据关键字切分肯定不行,没有对应的约定。
- 一个事务内必须是同一个数据源,不然事务会有问题
对于代码无入侵的方式我们还没有想出来,欢迎大家提供思路。
我们的做法
利用spring事务的默认传播属性,有事务则加入,没有事务则开启新事务。我们只需要在开启事务的入口处能判断出来即可。
覆盖事务管理器
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
public DynamicDataSourceTransactionManager() {
}
public DynamicDataSourceTransactionManager(DataSource dataSource) {
super(dataSource);
}
/**
* 只读事务到读库,读写事务到写库
* @param transaction
* @param definition
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
//设置数据源
boolean readOnly = definition.isReadOnly();
if(readOnly) {
DynamicDataSource.setDataSourceType(DatabaseType.zichan360_case_read);
} else {
DynamicDataSource.setDataSourceType(DatabaseType.zichan360_case);
}
super.doBegin(transaction, definition);
}
/**
* 清理本地线程的数据源
* @param transaction
*/
@Override
protected void doCleanupAfterCompletion(Object transaction) {
super.doCleanupAfterCompletion(transaction);
DynamicDataSource.clearCustomerType();
}
}
//配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DynamicDataSourceTransactionManager(dataSource);
}
配置代码@Transactional(readOnly = true) 一起使用