自定義註解實現數據緩存與定時重載

公司實際項目中,有一個程序把很多配置在記錄在了數據庫的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基本不需要再去修改了,這樣以來也能減少新增配置帶來的代碼編寫量,畢竟偷懶纔是進步的源泉嘛!



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章