公司實際項目中,有一個程序把很多配置在記錄在了數據庫的t_config表中,方便操作人員通過前端頁面修改這些配置
前期開發人員並沒有對這個表進行緩存,每個交易都實時的讀取數據庫中的相應配置
隨着業務量不斷增大,發現性能越來越低,排查後發現是上述的過於頻繁的讀取數據庫,
由於公司暫時不接入redis等技術,因此我們就自己寫了一些緩存與重載緩存的方法,將這些頻繁讀取的數據庫信息緩存到內存中,加快了讀取速度,減少了數據庫操作
下面簡單介紹一下這個方案,首先是表結構
CREATE TABLE `t_config` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`NAME` varchar(60) NOT NULL,
`DATA` varchar(255) DEFAULT NULL,
`REMARK` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ID`)
)
對應的JavaBean
public class TConfig implements Serializable, Cloneable {
private static final long serialVersionUID = 1619811684138791L;
private Long id;
private String name;
private String data;
private String remark;
//………………省略get set 和構造器
//JDBCTEMPLATE的映射
public static class TConfigMapper implements RowMapper<TConfig> {
public TConfig mapRow(ResultSet rs, int index) throws SQLException {
TConfig tConfig = new TConfig();
Long id = rs.getLong("id");
tConfig.setId(id);
String name = rs.getString("name");
tConfig.setName(name);
String data = rs.getString("data");
tConfig.setData(data);
String remark = rs.getString("remark");
tConfig.setRemark(remark);
return tConfig;
}
}
}
項目使用到的是mysql,jre版本1.7,spring4.3.4大家族,orm用的是spring的jdbcTemplate,數據源是c3p0
接下來介紹註解,該註解很簡單,只有兩個字段,name代表是數據庫中的name,不指定則默認爲當前屬性的名稱
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TConfigCache {
String name() default "";
/**
* 重載間隔時間,單位是分鐘,默認5分鐘,最小值是1分鐘
*
* @return
*/
int intervalTime() default 5;
}
這裏先看看數據庫保存的這些測試數據
接下來是ConfigsFromDB,所有的數據庫配置都配在這個抽象類中,這個類只需聲明變量,和指定get方法即可,無需關心變量是如何賦值與定時重載的,注意註解的值與上圖的數據庫數據
public abstract class ConfigsFromDB {
@TConfigCache
private String IS_SEND;
@TConfigCache
private int START_TIME; //支持基本數據類型
@TConfigCache
private Integer END_TIME;
@TConfigCache(name = "Threshold_Amount",intervalTime = 1)
private Double qiDianJinE; //起點金額
private String string; //普通屬性,沒有被註解
/**
* 加載完數據後回調,能做一些字段的善後工作,但不是必須的
*/
protected void afterLoad(){
if(qiDianJinE == null){
qiDianJinE = 0.0;
}
}
/**
* 加載完數據後被回調,做一些數據有效性檢查,但不是必須的
* @return
*/
protected boolean checkData(){
if(START_TIME >= END_TIME){
return false;
}
return true;
}
public String getIS_SEND() {
return IS_SEND;
}
public Integer getSTART_TIME() {
return START_TIME;
}
public Integer getEND_TIME() {
return END_TIME;
}
public Double getQiDianJinE() {
return qiDianJinE;
}
public String getString() {
return string;
}
}
接下來是重頭戲,ConfigsFromDBImpl,繼承了ConfigsFromDB,用於識別TConfigCache註解,以及爲這些屬性賦值,定時重載等
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Repository;
@Repository("configs")
public class ConfigsFromDBImpl extends ConfigsFromDB {
@Autowired
private JdbcTemplate jdbcTemplate;
private List<MyField> fields = new ArrayList<MyField>();
private final String SQL = "select * from t_config";
/**
* 基本數據類型的裝箱類名轉換
*/
final private static Map<String, String> TYPE_MAP = new HashMap<String, String>();
static {
TYPE_MAP.put("int", Integer.class.getSimpleName());
TYPE_MAP.put("double", Double.class.getSimpleName());
TYPE_MAP.put("short", Short.class.getSimpleName());
TYPE_MAP.put("long", Long.class.getSimpleName());
TYPE_MAP.put("byte", Byte.class.getSimpleName());
TYPE_MAP.put("float", Float.class.getSimpleName());
TYPE_MAP.put("boolean", Boolean.class.getSimpleName());
TYPE_MAP.put("char", Character.class.getSimpleName());
}
@PostConstruct
private void init() throws Exception {
initAnno();
if (!load()) {
// 拋出Exception,會讓spring初始化失敗,程序無法啓動
throw new Exception("初始化ConfigsFromDB失敗,程序無法啓動");
}
}
private boolean load() throws Exception { //加載數據,並賦值
System.out.println("開始加載數據庫配置表");
long startTime = System.currentTimeMillis();
List<TConfig> configs = jdbcTemplate.query(SQL, new TConfig.TConfigMapper());//執行sql,全表查詢
int length = fields.size();
for (int i = 0; i < length; i++) {
MyField myField = fields.get(i);
Field field = myField.field;
TConfigCache anno = field.getDeclaredAnnotation(TConfigCache.class);
String name = field.getName();
if (!"".equals(anno.name())) { // 如果註解的name不是空,就取註解的
name = anno.name();
}
if (isTimeOut(myField, anno.intervalTime())) { // 判斷是否超時
for (TConfig config : configs) {
if (config.getName().equals(name)) {
setValue(field, config.getData()); //給屬性賦值
myField = new MyField(field, System.currentTimeMillis()); //重新設定時間
fields.set(i, myField); //放回原來的位置
configs.remove(config); //從當前數據庫查詢結果移走,可以減少後來者的遍歷次數
System.out.println(field.getName()+"獲取到的數據庫值爲"+config.getData());
break;
}
}
} else {
System.out.println(field.getName() + "屬性未超時,無需重載");
continue;
}
}
afterLoad(); //回調數據後期加工方法
System.out.println("加載數據庫配置表結束,耗時:" + (System.currentTimeMillis() - startTime));
return checkData(); //回調數據檢查方法
}
private void initAnno() { // 尋找有被TConfig註解的屬性
for (Field field : this.getClass().getSuperclass().getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(TConfigCache.class)) { // 判斷是不是TConfigCache註解過的
MyField myField = new MyField(field, 0);
fields.add(myField);
System.out.println("成功識別到" + field.getName() + "屬性被註解");
}
}
}
/**
* 給屬性賦值,能根據不同屬性的類型,做對應的String轉換
*/
private void setValue(Field field, String value) throws IllegalAccessException {
String type = field.getType().getSimpleName();
if (field.getType().isPrimitive()) { // 判斷是否爲int,long等基本數據類型,是的話則要獲得其裝箱的類型
type = TYPE_MAP.get(type);
}
switch (type) { // 注意jdk1.7以上才支持這個這種語法
case "Integer":
field.set(this, Integer.parseInt(value));
break;
case "Double":
field.set(this, Double.parseDouble(value));
break;
case "Short":
field.set(this, Short.parseShort(value));
break;
case "Long":
field.set(this, Long.parseLong(value));
break;
case "Byte":
field.set(this, Byte.parseByte(value));
break;
case "Character":
field.set(this, value);
break;
case "Boolean":
field.set(this, Boolean.parseBoolean(value)); // 對於這個轉換,可以額外自定義,比如1或者ON代表true
break;
// 可以繼續添加與String類型轉換,如Date、BigInteger等
default:
field.set(this, value);
}
}
/**
* 判斷這個屬性是否超時
*
* @param myField
* @return
*/
private boolean isTimeOut(MyField myField, int intervalTime) {
long lastLoadTime = myField.lastLoadTime;
if (System.currentTimeMillis() < (lastLoadTime + (intervalTime * 60 * 1000))) {
return false;
}
return true;
}
@SuppressWarnings("unused")
private class MyField { // 對Field進行簡單封裝,使其有“上次加載時間”的屬性
Field field;
long lastLoadTime = 0;
public MyField(Field field, long lastLoadTime) {
super();
this.field = field;
this.lastLoadTime = lastLoadTime;
}
public MyField(Field field) {
super();
this.field = field;
}
public MyField() {
super();
}
}
/**
* 定時加載數據
* @throws Exception
*/
@Scheduled(fixedDelay = 1 * 60 * 1000, initialDelay = 2 * 60 * 1000) // 1分鐘定時調起,初始調起休眠2分鐘
private void loadByTime() throws Exception {
System.out.println("@Scheduled開始重載數據");
load();
System.out.println("@Scheduled開始重載數據完成");
}
}
最後是使用的演示
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) throws Throwable {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("./config/applicationContext.xml");
final ConfigsFromDB configs = (ConfigsFromDB) applicationContext.getBean("configs");
new Thread() { //定時打印緩存的數據
public void run() {
while (true) {
System.out.println("-----------------");
System.out.println(configs.getIS_SEND());
System.out.println(configs.getSTART_TIME());
System.out.println(configs.getEND_TIME());
System.out.println(configs.getQiDianJinE());
System.out.println("-----------------");
try {
sleep(65 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
}
}
第一次執行結果,第一次執行需要創建數據庫連接等,因此速度較慢,耗時長
執行第一次之後,我修改了數據庫的數據,使得起點金額變爲9.99,在定時任務執行後,能看到打印信息如下
能看到起點金額已經修改成功了,緩存也刷新正確,此外,重新加載的耗時比起第一次是明顯減少了
在程序穩定之後,有什麼新的配置需要添加,就直接在ConfigsFromDB類中新增屬性,指定get方法即可,ConfigsFromDBImpl基本不需要再去修改了,這樣以來也能減少新增配置帶來的代碼編寫量,畢竟偷懶纔是進步的源泉嘛!