公司實際項目中,有一個程式把很多配置在記錄在了資料庫的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 {@TConfigCacheprivate String IS_SEND;@TConfigCacheprivate int START_TIME; //支援基礎資料型別 (Elementary Data Type)@TConfigCacheprivate 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 {@Autowiredprivate JdbcTemplate jdbcTemplate;private List<MyField> fields = new ArrayList<MyField>();private final String SQL = "select * from t_config";/** * 基礎資料型別 (Elementary Data Type)的裝箱類名轉換 */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());}@PostConstructprivate 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等基礎資料型別 (Elementary Data Type),是的話則要獲得其裝箱的類型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代表truebreak;// 可以繼續添加與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基本不需要再去修改了,這樣以來也能減少新增配置帶來的代碼編寫量,畢竟偷懶才是進步的源泉嘛。