由于Cache没有什么表数据大了查询和插入性能下降的问题,而关系库在数据量上千万后会性能下降,之前关注点都是Java业务脚本化和开发部署简单,还没管关系库单表大问题和级联查询复杂后慢的问题,现在开始解决这俩问题,这是第一阶段实现。
首先对单表数据太多导致性能下降的问题还是采用历史表解决,既为主业务建立几个甚至上10个历史表,历史表的主键不自增,然后ORM提供API把数据迁移到历史表。
首先给表注解加上维护历史表的地方和历史数据切割所用的字段:
实现两个兼容查历史数据的API给业务
/**
* 根据条件+字段查询,查询结果按指定的页面把数据按List返回。由ORM根据参数决定顺带查询所有历史数据或者最近历史数据,或者不查历史数据
* 开发者为主业务表建立同结果非自增主键的表来供业务数据往历史表迁移,在实体特性维护所有历史表,历史表按每个存满指定数据后换下一个历史表,
* ORM按历史表依次调用查询合并结果
* 以此解决单表数据太大查询和插入慢问题
* 不分页
*
* @param findHisNum 查询历史表的数量,0:不查历史表 大于0的数就是最多查从最近使用历史表往前推的指定数量的历史表
* @param model 实体对象
* @param param 查询条件参数,数据列名和值的键对
* @param orderFields 排序字段,如RowID Desc
* @param pageSize 页大小。为-1,无条件查所有数据
* @param pageIndex 第几页。为-1,无条件查询所有数据
* @param fields 显示字段,为空显示所有列,字段名称以英文','隔开,如:RowID,Code,Name
* @param joiner 连接符,为空或不给则查询条件以且连接,给的话长度比参数少1
* @param operators 操作符,为空或不给的话条件以等来判断,给的话与参数长度一致。如!=,<,>
* @param 限定实体类型
* @return 查询实体列表List
*/
public <T> List<T> DolerFindAll(int findHisNum, T model, List<ParamDto> param, String orderFields, int pageSize, int pageIndex, String fields, List<String> joiner, List<String> operators) throws Exception
{
//存储返回的对象数据
List<T> retList = new ArrayList<T>();
//查询起始行数
int fromRow = -1;
//查询结束行数
int toRow = -1;
//是否查询全部数据
boolean findAll = false;
//记录总行数
int rowCount = 0;
//处理显示字段
if (fields != null && !fields.isEmpty()) {
fields = "," + fields + ",";
}
//如果未传入分页数据其中一个未-1,则认为部分页而查询所有数据
if (pageIndex == -1 || pageSize == -1) {
findAll = true;
}
//计算查询起始和结束行数
else {
fromRow = (pageIndex - 1) * pageSize;
toRow = pageIndex * pageSize;
}
PreparedStatement pstat = null;
ResultSet rst = null;
//表信息
JRT.DAL.ORM.Common.TableInfo tableInfo = JRT.DAL.ORM.Common.ModelToSqlUtil.GetTypeInfo(model);
List<TableInfo> findTableList=new ArrayList<>();
findTableList.add(tableInfo);
if(findHisNum>0)
{
String HisTableName=tableInfo.TableInfo.HisTableName();
if(!HisTableName.isEmpty())
{
String [] HisTableNameArr=HisTableName.split("^");
boolean StartCal=false;
for(int h=HisTableNameArr.length-1;h>=0;h--)
{
String HisModelName=HisTableNameArr[h];
Class cHis = GetTypeByName(HisModelName);
if (cHis != null) {
Object oHis = cHis.getConstructor().newInstance();
String UseHisTableName=TryGetUseHisTableName(model.getClass().getSimpleName(),oHis);
if(UseHisTableName!=null&&!UseHisTableName.isEmpty())
{
StartCal=true;
}
if(StartCal==true&&findHisNum>0) {
findHisNum--;
TableInfo tableInfoHis = JRT.DAL.ORM.Common.ModelToSqlUtil.GetTypeInfo(oHis);
findTableList.add(tableInfoHis);
}
}
}
//所有的历史表都没数据那么认为第一个历史表在用
if(StartCal==false)
{
CurHisTable.put(model.getClass().getSimpleName(),"");
}
}
}
//循环查多个表的数据
for(TableInfo tbInfo:findTableList) {
//根据表信息将查询参数组装成Select SQL
String sql = JRT.DAL.ORM.Common.ModelToSqlUtil.GetSelectSqlByTableInfo(Manager().GetIDbFactory(factoryName), tbInfo, param, operators, joiner, orderFields, false, -1);
//写SQL日志
JRT.Core.Util.LogUtils.WriteSqlLog("执行QueryAll返回List查询SQL:" + sql);
Class<?> clazzz = model.getClass();
try {
pstat = Manager().Connection().prepareStatement(sql);
String paraSql = DBParaUtil.SetDBPara(pstat, param);
rst = pstat.executeQuery();
JRT.Core.Util.LogUtils.WriteSqlLog("参数:" + paraSql);
while (rst.next()) {
rowCount++; //总行数加一
//查询全部,或者取分页范围内的记录
if (findAll || (rowCount > fromRow && rowCount <= toRow)) {
T obj = (T) clazzz.getConstructor().newInstance();
for (int coli = 0; coli < tbInfo.ColList.size(); coli++) {
String name = tbInfo.ColList.get(coli).Name;
Object value = rst.getObject(name);
JRT.Core.Util.ReflectUtil.SetObjValue(obj, name, value);
}
retList.add(obj);
}
}
} catch (Exception ex) {
//查询异常清空数据
retList.clear();
throw ex;
}
//操作结束释放资源,但是不断连接,不然没法连续做其他数据库操作了
finally {
if (rst != null) {
rst.close();
}
if (pstat != null) {
pstat.close();
}
//如果上层调用未开启事务,则调用结束释放数据库连接
if (!Manager().Hastransaction) {
manager.Close();
}
}
}
return retList;
}
/**
* 根据条件+字段查询,查询结果按指定的页面把数据按JSON返回;
* 开发者为主业务表建立同结果非自增主键的表来供业务数据往历史表迁移,在实体特性维护所有历史表,历史表按每个存满指定数据后换下一个历史表
* ORM按历史表依次调用查询合并结果
* 以此解决单表数据太大查询和插入慢问题
* 该方法不带分页
* @param findHisNum 查询历史表的数量,0:不查历史表 大于0的数就是最多查从最近使用历史表往前推的指定数量的历史表
* @param model 实体对象
* @param param 查询条件参数,数据列名和值的键对
* @param orderFields 排序字段,如RowID Desc
* @param returnCount 是否输出数据总行数
* @param pageSize 页大小。为-1,无条件查所有数据
* @param pageIndex 第几页。为-1,无条件查询所有数据
* @param fields 显示字段,为空显示所有列,字段名称以英文','隔开,如:RowID,Code,Name
* @param joiner 连接符,为空或不给则查询条件以且连接,给的话长度比参数少1
* @param operators 操作符,为空或不给的话条件以等来判断,给的话与参数长度一致。如!=,<,>
* @param top 查询返回的行数,-1就返回所有行
* @return 查询json串
*/
public <T> String DolerQueryAllTop(int findHisNum,T model, List<ParamDto> param, String orderFields, boolean returnCount, int pageSize, int pageIndex, String fields, List<String> joiner, List<String> operators, int top) throws Exception
{
//json数据组装容器
StringBuilder jsonsb = new StringBuilder();
//查询起始行数
int fromRow = -1;
//查询结束行数
int toRow = -1;
//是否查询全部数据
boolean findAll = false;
//记录总行数
int rowCount = 0;
//处理显示字段
if (fields != null && !fields.isEmpty()) {
fields = "," + fields + ",";
}
//如果未传入分页数据其中一个未-1,则认为部分页而查询所有数据
if (pageIndex == -1 || pageSize == -1) {
findAll = true;
}
//计算查询起始和结束行数
else {
fromRow = (pageIndex - 1) * pageSize;
toRow = pageIndex * pageSize;
}
PreparedStatement pstat = null;
ResultSet rst = null;
JRT.DAL.ORM.Common.TableInfo tableInfo = JRT.DAL.ORM.Common.ModelToSqlUtil.GetTypeInfo(model);
List<TableInfo> findTableList=new ArrayList<>();
findTableList.add(tableInfo);
if(findHisNum>0)
{
String HisTableName=tableInfo.TableInfo.HisTableName();
if(!HisTableName.isEmpty())
{
String [] HisTableNameArr=HisTableName.split("^");
boolean StartCal=false;
for(int h=HisTableNameArr.length-1;h>=0;h--)
{
String HisModelName=HisTableNameArr[h];
Class cHis = GetTypeByName(HisModelName);
if (cHis != null) {
Object oHis = cHis.getConstructor().newInstance();
String UseHisTableName=TryGetUseHisTableName(model.getClass().getSimpleName(),oHis);
if(UseHisTableName!=null&&!UseHisTableName.isEmpty())
{
StartCal=true;
}
if(StartCal==true&&findHisNum>0) {
findHisNum--;
TableInfo tableInfoHis = JRT.DAL.ORM.Common.ModelToSqlUtil.GetTypeInfo(oHis);
findTableList.add(tableInfoHis);
}
}
}
//所有的历史表都没数据那么认为第一个历史表在用
if(StartCal==false)
{
CurHisTable.put(model.getClass().getSimpleName(),HisTableNameArr[0]);
}
}
}
//如果返回总行数,返回总行数写法
if (returnCount) {
jsonsb.append("{");
jsonsb.append("\"rows\":[");
}
//否则采用普通数组写法
else {
jsonsb.append("[");
}
StringBuilder rowAllsb = new StringBuilder();
//循环查多个表的数据
for(TableInfo tbInfo:findTableList) {
//根据表信息将查询参数组装成Select SQL
String sql = JRT.DAL.ORM.Common.ModelToSqlUtil.GetSelectSqlByTableInfo(Manager().GetIDbFactory(factoryName), tbInfo, param, operators, joiner, orderFields, false, top);
//写SQL日志
JRT.Core.Util.LogUtils.WriteSqlLog("执行QueryAll返回String查询SQL:" + sql);
try {
pstat = Manager().Connection().prepareStatement(sql);
String paraSql = DBParaUtil.SetDBPara(pstat, param);
rst = pstat.executeQuery();
JRT.Core.Util.LogUtils.WriteSqlLog("参数:" + paraSql);
//标识是否第一行
boolean isFirstRow = true;
while (rst.next()) {
rowCount++; //总行数加一
//查询全部,或者取分页范围内的记录
if (findAll || (rowCount > fromRow && rowCount <= toRow)) {
ResultSetMetaData metaData = rst.getMetaData();
//获取列数
int colCount = metaData.getColumnCount();
//单行数据容器
StringBuilder rowsb = new StringBuilder();
rowsb.append("{");
//标识是否第一列
boolean isFirstCol = true;
for (int coli = 1; coli <= colCount; coli++) {
//获取列名
String colName = metaData.getColumnName(coli);
//获取列值
Object colValue = rst.getObject(coli);
if (colValue == null) colValue = "";
//如果传了显示的字段,过滤不包含的字段
if (fields != null && !fields.isEmpty() && fields.indexOf("," + colName + ",") < 0) {
continue;
}
if (isFirstCol) {
rowsb.append("\"" + colName + "\":");
rowsb.append("\"" + colValue + "\"");
isFirstCol = false;
} else {
//非第一列插入","
rowsb.append(",");
rowsb.append("\"" + colName + "\":");
rowsb.append("\"" + colValue + "\"");
}
}
rowsb.append("}");
if (isFirstRow) {
rowAllsb.append(rowsb.toString());
isFirstRow = false;
} else {
rowAllsb.append(",");
rowAllsb.append(rowsb.toString());
}
}
}
} catch (Exception ex) {
//查询异常清空数据记录容器
rowAllsb.delete(0, rowAllsb.length());
throw ex;
}
//操作结束释放资源,但是不断连接,不然没法连续做其他数据库操作了
finally {
if (rst != null) {
rst.close();
}
if (pstat != null) {
pstat.close();
}
//如果上层调用未开启事务,则调用结束释放数据库连接
if (!Manager().Hastransaction) {
manager.Close();
}
}
}
//组装数据记录
jsonsb.append(rowAllsb.toString());
//补充数组结尾符
jsonsb.append("]");
if (returnCount) {
jsonsb.append(",");
jsonsb.append("\"total\":");
jsonsb.append(rowCount);
jsonsb.append("}");
}
return jsonsb.toString();
}
对应$get的实现,首先抽取节点对象,记录每个数据的时间
package JRT.DAL.ORM.Global;
/**
* 一个global的管理节点
*/
public class OneGlobalNode {
/**
* Java内部时间
*/
public Long Time;
/**
* 对象数据
*/
public Object Data;
}
实现缓存管理
package JRT.DAL.ORM.Global;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import JRT.Core.MultiPlatform.JRTConfigurtaion;
import JRT.Core.Util.LogUtils;
import JRT.DAL.ORM.Global.OneGlobalNode;
/**
* 实现内存模拟global的效果
*/
public class GlobalManager {
/**
* 在内存里缓存热点数据
*/
private static ConcurrentHashMap<String, ConcurrentHashMap<String, OneGlobalNode>> AllHotData = new ConcurrentHashMap<>();
/**
* 要缓存数据的队列
*/
private static ConcurrentLinkedDeque TaskQuen = new ConcurrentLinkedDeque();
/**
* 管理缓存的定时器
*/
private static Timer ManageTimer = new Timer();
/**
* 缓存的最大对象数量
*/
public static Integer GlobalCacheNum = 100000;
/**
* 当前的缓存数量
*/
private static AtomicInteger CurCacheNum=new AtomicInteger(0);
/**
* 最后删除数据的时间
*/
private static Long LastDeleteTime=null;
/**
* 加入缓存,直接缓存,具体的后续有缓存管理器线程维护缓存,这里只管加入队列即可
*
* @param obj
* @throws Exception
*/
public static void InCache(Object obj) throws Exception{
TaskQuen.add(JRT.Core.Util.JsonUtil.CloneObject(obj));
}
/**
* 通过主键查询数据
* @param model
* @param id
* @param
* @return
* @throws Exception
*/
public static <T> T DolerGet(T model,Object id) throws Exception
{
//实体的名称
String modelName = model.getClass().getName();
if(AllHotData.containsKey(modelName))
{
//命中数据,克隆返回
if(AllHotData.get(modelName).containsKey(id))
{
OneGlobalNode node=AllHotData.get(modelName).get(id);
//更新时间
node.Time=JRT.Core.Util.TimeParser.GetTimeInMillis();
Object retObj=JRT.Core.Util.JsonUtil.CloneObject(node.Data);
return (T)retObj;
}
}
return null;
}
/**
* 启动缓存数据管理的线程
*/
public static void StartGlobalManagerTask() throws Exception{
//最大缓存数量
String GlobalCacheNumConf = JRTConfigurtaion.Configuration("GlobalCacheNum");
if (GlobalCacheNumConf != null && !GlobalCacheNumConf.isEmpty()) {
GlobalCacheNum = JRT.Core.Util.Convert.ToInt32(GlobalCacheNumConf);
}
//定时任务
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
try {
//缓存队列的数据并入缓存
while (TaskQuen.size() > 0) {
//处理要加入缓存的队列
DealOneDataQuen();
}
//清理多余的缓存数据,这里需要讲究算法,要求在上百万的缓存数据里快速找到时间最久远的数据
if(CurCacheNum.get()>GlobalCacheNum)
{
//每轮清理时间处于上次清理时间和当前时间前百分之5的老数据
long Diff=(JRT.Core.Util.TimeParser.GetTimeInMillis()-LastDeleteTime)/20;
//留下数据的最大时间
long LeftMaxTime=LastDeleteTime+Diff;
//遍历所有的热点数据
for (String model : AllHotData.keySet()) {
ConcurrentHashMap<String, OneGlobalNode> oneTableHot=AllHotData.get(model);
//记录要删除的数据
List<String> delList=new ArrayList<>();
for (String key : oneTableHot.keySet()) {
OneGlobalNode one=oneTableHot.get(key);
//需要删除的数据
if(one.Time<LeftMaxTime)
{
delList.add(key);
}
}
//移除时间久的数据
for(String del:delList)
{
oneTableHot.remove(del);
}
}
}
//清理时间久远的缓存数据
} catch (Exception ex) {
LogUtils.WriteExceptionLog("处理Global缓存异常", ex);
}
}
};
ManageTimer.schedule(timerTask, 0, 500);
}
/**
* 处理队列里的一条数据并入缓存
*/
private static void DealOneDataQuen() {
try {
Object obj = TaskQuen.pop();
if (obj != null) {
JRT.DAL.ORM.Common.TableInfo tableInfo = JRT.DAL.ORM.Common.ModelToSqlUtil.GetTypeInfo(obj);
//实体的名称
String modelName = obj.getClass().getName();
//得到数据的主键
String id = tableInfo.ID.Value.toString();
if (!AllHotData.containsKey(modelName)) {
ConcurrentHashMap<String, OneGlobalNode> map = new ConcurrentHashMap<>();
AllHotData.put(modelName, map);
}
//更新数据
if (AllHotData.get(modelName).containsKey(id)) {
AllHotData.get(modelName).get(id).Data = obj;
AllHotData.get(modelName).get(id).Time = JRT.Core.Util.TimeParser.GetTimeInMillis();
}
//加入到缓存
else {
OneGlobalNode node = new OneGlobalNode();
node.Data = obj;
node.Time = JRT.Core.Util.TimeParser.GetTimeInMillis();
AllHotData.get(modelName).put(id, node);
//缓存数量加1
CurCacheNum.addAndGet(1);
//记录时间
if(LastDeleteTime==null)
{
LastDeleteTime=JRT.Core.Util.TimeParser.GetTimeInMillis();
}
}
}
} catch (Exception ex) {
LogUtils.WriteExceptionLog("处理Global缓存添加异常", ex);
}
}
}
实现DolerGet方法
/**
* 通过主键查询数据,带缓存的查询,用来解决关系库的复杂关系数据获取,顶替Cache的$g
* @param model 实体
* @param id 主键
* @param
* @return
* @throws Exception
*/
public <T> T DolerGet(T model,Object id) throws Exception
{
T ret=GlobalManager.DolerGet(model,id);
//命中缓存直接返回
if(ret!=null)
{
return ret;
}
else
{
//调用数据库查询
ret=GetById(model,id);
//通知存入缓存
GlobalManager.InCache(ret);
}
return ret;
}
修改数据的API尝试把实体推入缓存队列
对事务时候在提交事务才推入缓存队列
网站初始化时候启动缓存管理器
配置缓存数量和历史表换表的大小
这样以一天标本两万的客户算,缓存两天的热点数据应该不会超过100万,和Global一样的给他10-20G的内存做缓存用就行了,这样理论上应该可以在SQL查询的主业务数据后其他分支数据都借助DolerGet得到数据,这里ORM增、删、改方法还没调加入缓存逻辑,增删改之后调用GlobalManager.InCache(ret);再结合TCP通知主站点分发增、删、改信息后更新缓存,缓存能命中的数据就百分百是准确的最新数据,就能达到极高的Get效率,解决关系库复杂维护的问题,思想上高度借鉴Global的$get和缓存思想。缓存的难点一直是怎么把热点数据挑出来,而通过自己实现ORM,然后增、删、改和DolerGet的数据都抛入缓存队列,就可以准确得到热点数据,因为修改的数据和DolerGet请求的数据本身就是最近活动的,然后缓存管理器再把业务放入队列的数据并入缓存,同时清理时间久远的数据,这样内存里的数据就停了的都是热点。