利用redis做業務緩存和配置項
來自:https://github.com/018/RedisCache
背景
從以前的C/S應用到現在的B/S,系統配置項都是必不可少的。一般都有一個SettingUtils類,提供read和write方法,然后就一大堆作為Key的常量。通過這樣來實現:
String ip = SettingUtils.read(SettingConsts.IP);//獲取ip
SettingUtils.write(SettingConsts.IP, "127.0.0.1");//設置ip
然而,現在並發要求越來越高,緩存是個標配。無論是業務數據還是配置項,都可以往緩存里扔。緩存,也離不開Key,一大堆作為Key的常量。治理這些Key是個大問題。
遇到動態代理
動態代理,早些年就了解過,可一直沒真正用到項目里,直到一次研究了一下mybatis源代碼,發現其核心代碼就是動態代理。那什么是動態代理呢?我就不詳細解釋了,對它不了解的還是乖乖的 百度一下動態代理 。這里從網上投了一張圖,如下:
它大概就是我們可以動態的自定義的控制實現。給你Object proxy、Method method、Object[] args三個參數,然后你自己決定怎么實現。給個簡單的例子:
/**
* 接口
*/
public interface Something {
String get(String key);
String set(String key, String value);
}
/**
* 調用處理器
*/
public class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + " doing ...");
System.out.println("proxy is " + proxy.getClass().getName());
System.out.println("method is " + method.getName());
System.out.println("args is " + Arrays.toString(args));
System.out.println(method.getName() + " done!");
return method.getName() + " invoked";
}
}
public class Test {
public static void main(String[] args) {
Something somethingProxy = (Something) java.lang.reflect.Proxy.newProxyInstance(Something.class.getClassLoader(),
new Class<?>[]{Something.class},
new MyInvocationHandler());
System.out.println("somethingProxy.get(\"name\"): " + somethingProxy.get("name"));
System.out.println();
System.out.println("somethingProxy.set(\"name\", \"018\"): " + somethingProxy.set("name", "018"));
System.out.println();
}
}
以上代碼的輸出結果:
get doing ...
proxy is com.sun.proxy.$Proxy0
method is get
args is [name]
get done!
somethingProxy.get("name"): get invoked
set doing ...
proxy is com.sun.proxy.$Proxy0
method is set
args is [name, 018]
set done!
somethingProxy.set("name", "018"): set invoked
- 通過Proxy.newProxyInstance創建一個Something的代理對象somethingProxy。
- 通過somethingProxy實例調用其方法get/set時,會執行MyInvocationHandler.invoke方法。
思考
緩存,通過Key,返回值。
動態代理,通過Method(方法),執行返回值。
怎么把它們關聯起來呢?方法有方法名,那能不能把Method method的方法名對應到Key?能!!!
方案
在最開始的例子獲取ip就應該這樣寫:
public interface DataSourceSettings {
String getIp();
void setIp(String ip);
int getPort();
void setPort(int port);
// 其他項 ...
}
public class SettingsInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// TODO:
// 1、去掉get/set,截圖后面的字符串作為Key
// 2、redis客戶端通過Key獲取值返回
}
}
配置項完美解決了。
但業務緩存呢?相對來說,配置項的Key是固定的,而業務緩存的Key則是不固定的。比如緩存商品信息,商品id為001和002等等,得緩存不同的Key。就得有一個動態Key的解決方案,即productCaches.put(商品id, 商品實體)
這樣的實現方式。
參考spring-data-redis的BoundHashOperations,可以對其進行擴展實現這一功能。這樣我們就可以這樣定義一個商品緩存接口:
public interface HashSessionOperations<HK, HV> extends BoundHashOperations<String, HK, HV> {
}
public interface ProductCaches extends HashSessionOperations {
}
緩存數據和獲取數據則如下:
productCaches.put(product1.prod_id, product1);//緩存數據
Product product = (Product)productCaches.get(prod_id);//獲取緩存數據
至此,業務緩存也完美解決。
當然,我們對BoundListOperations、BoundSetOperations、BoundValueOperations、BoundZSetOperations進行對應的擴展。這樣,這些不僅僅做業務緩存,也可以用它來作為redis的一個客戶端使用。
看到這里,只看到了接口,也即是結果,知道了怎么使用它應用到項目中。但,怎么實現的呢?但是是動態代理。來,廢話不多說,兩個InvocationHandler碼上來:
/**
* 簡單的InvocationHandler
* 主要用於執行配置項
*/
public class SimpleSessionOperationInvocationHandler implements InvocationHandler {
private static final String METHOD_SET = "set";
private static final String METHOD_GET = "get";
private static final String METHOD_TOSTRING = "toString";
private DefaultSimpleSessionOperations defaultSimpleSessionOperations;
public SimpleSessionOperationInvocationHandler(DefaultSimpleSessionOperations defaultSimpleSessionOperations) {
this.defaultSimpleSessionOperations = defaultSimpleSessionOperations;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> cls = method.getDeclaringClass();
String group = cls.getSimpleName().replace("Settings", "").replace("Setting", "");
String methodName = method.getName();
String methodString = null;
String item = null;
Object value = null;
if (METHOD_TOSTRING.equals(methodName)) {
return cls.getName();
} else if (METHOD_SET.equals(methodName)) {
// void set(String item, ? value)
if (method.getParameterCount() != 2 || !method.getParameterTypes()[0].getSimpleName().equals(String.class.getSimpleName()) ||
!method.getParameterTypes()[1].getSimpleName().equals(Object.class.getSimpleName()) ||
args == null || args.length != 2) {
throw new NonsupportMethodException(cls.getPackage().getName(), cls.getSimpleName(), methodName,
"方法聲明錯誤,正確為 void set(String key, ? value)。");
}
methodString = METHOD_SET;
item = args[0].toString();
value = args[1];
} else if (METHOD_GET.equals(methodName)) {
// ? get(String item)
if (method.getParameterCount() != 1 || !method.getParameterTypes()[0].getSimpleName().equals(String.class.getSimpleName()) ||
args == null || args.length != 1) {
throw new NonsupportMethodException(cls.getPackage().getName(), cls.getSimpleName(), methodName,
"方法聲明錯誤,正確為 ? get(String item)。");
}
methodString = METHOD_GET;
item = args[0].toString();
} else if (methodName.startsWith(METHOD_SET)) {
// void setXXX(? value)
if (method.getParameterCount() != 1 ||
args == null || args.length != 1) {
throw new NonsupportMethodException(cls.getPackage().getName(), cls.getSimpleName(), methodName,
"方法聲明錯誤,正確為 void setXXX(? value)。");
}
methodString = METHOD_SET;
item = methodName.substring(METHOD_SET.length());
value = args[0];
} else if (methodName.startsWith(METHOD_GET)) {
// Object getXXX()
if (method.getParameterCount() != 0 ||
(args != null && args.length != 0)) {
throw new NonsupportMethodException(cls.getPackage().getName(), cls.getSimpleName(), methodName,
"方法聲明錯誤,正確為 Object getXXX()。");
}
methodString = METHOD_GET;
item = methodName.substring(METHOD_GET.length());
} else {
throw new NonsupportMethodException(cls.getPackage().getName(), cls.getSimpleName(), methodName,
"不支持的方法,只能是void set(String item, ? value)、? get(String item)、void setXXX(? value)、? getXXX()。");
}
switch (methodString) {
case (METHOD_GET):
Object val = this.defaultSimpleSessionOperations.get(group, item);
return val;
case (METHOD_SET):
this.defaultSimpleSessionOperations.put(group, item, value);
}
return null;
}
}
/**
* redis操作動態代理執行類
* 主要用於執行業務緩存
*/
public class RedisSessionOperationInvocationHandler implements InvocationHandler {
private static final String METHOD_TOSTRING = "toString";
BoundKeyOperations<?> sessionOperations; // 具體執行的redis對象
public RedisSessionOperationInvocationHandler(BoundKeyOperations<?> sessionOperations) {
this.sessionOperations = sessionOperations;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> cls = method.getDeclaringClass();
String methodName = method.getName();
Class<?> targetCls = sessionOperations.getClass();
Method methodTarget = targetCls.getDeclaredMethod(methodName, method.getParameterTypes());
if (methodTarget == null) {
throw new NonsupportMethodException(cls.getPackage().getName(), cls.getSimpleName(), methodName,
"不支持" + methodName + "方法。");
}
if (METHOD_TOSTRING.equals(methodName)) {
return cls.getName();
}
Object result = methodTarget.invoke(sessionOperations, args);
return result;
}
}
至於接口創建代理,就交給ClassScannerConfigurer吧。
/**
* 類掃描配置類
*/
public class ClassScannerConfigurer implements InitializingBean, ApplicationContextAware, BeanFactoryAware, BeanNameAware {
/**
* 待掃描的包
*/
private String basePackage;
private String beanName;
private DefaultListableBeanFactory beanFactory;
private ApplicationContext applicationContext;
private SessionOperationsFactory sessionOperationsFactory;
private List<ClassInfo> classInfos;
public String getBasePackage() {
return basePackage;
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
public SessionOperationsFactory getSessionOperationsFactory() {
return sessionOperationsFactory;
}
public void setSessionOperationsFactory(SessionOperationsFactory sessionOperationsFactory) {
this.sessionOperationsFactory = sessionOperationsFactory;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.basePackage, "Property 'basePackage' is required");
// 掃描並創建接口的動態代理
ClassPathScanner scanner = new ClassPathScanner();
scanner.setResourceLoader(this.applicationContext);
Set<Class> classes = scanner.doScans(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
if (Objects.isNull(classInfos)) {
classInfos = new ArrayList<>(classes.size());
}
for (Class<?> cls : classes) {
Object proxyObject = ProxyManager.newProxyInstance(this.sessionOperationsFactory, cls);
String beanName = cls.getSimpleName().substring(0, 1).toLowerCase() + cls.getSimpleName().substring(1);
this.beanFactory.registerSingleton(beanName, proxyObject);
ClassInfo classInfo = new ClassInfo(beanName, cls, proxyObject);
classInfos.add(classInfo);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (DefaultListableBeanFactory) beanFactory;
}
}
怎么加載這些呢,交給spring吧。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--Redis 線程池配置 -->
<bean id="jpoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="200"></property>
<property name="testOnBorrow" value="true"></property>
</bean>
<!--連接工廠 -->
<bean id="connectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="127.0.0.1"></property>
<property name="port" value="6339"></property>
<property name="usePool" value="true"></property>
<property name="timeout" value="100000"></property>
<property name="poolConfig" ref="jpoolConfig"></property>
</bean>
<!--數據模板 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"></property>
</bean>
<!--redis template 緩存管理 -->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg ref="redisTemplate"></constructor-arg>
<property name="usePrefix" value="true"></property>
<property name="loadRemoteCachesOnStartup" value="true"></property>
</bean>
<!-- 上面是配置redis的,從這里開始才是 -->
<!-- 配置settingSessionFactory -->
<bean id="settingSessionFactory" class="com.o18.redis.cache.SessionOperationsFactory">
<constructor-arg ref="redisTemplate"/>
<constructor-arg value="ORDER"/>
</bean>
<!-- 掃描Setting -->
<bean class="com.o18.redis.cache.ClassScannerConfigurer">
<property name="sessionOperationsFactory" ref="settingSessionFactory" />
<property name="basePackage" value="com.o18.redis.cache.caches.*;com.o18.redis.cache.settings.*" />
</bean>
</beans>
優點
- 代碼統一管理。一個包是系統配置項com.**.settings.*,一個包是業務緩存com.**.caches.*。
- 配置項隨時改隨時生效。有些調優的參數,有些在特殊時期需要即時調整的。通過用web管理界面,友好的解決。
擴展
上面提到用web管理界面來即時修改配置項,即可以用一些特性,掃描所有配置項提供修改,分組、排序等等都是可以做到的。
還有,等等...
反思
- 安全。原來配置項安安全全的在properties文件躺着,這樣強行把它拉到安全的問題上來,當然祈禱redis安全!
- 默認方法不行。接口上有默認方法,那段代碼就相對於廢了。
- 性能。沒實際做過壓測,但鑒於mybatis,如果出現性能問題,那就是我寫的代碼需要優化,不是方案問題。
總結
通過mybatis的動態代理,實現基於redis的配置項即時修改生效。還擴展了業務緩存,使其代碼集中。該方案中核心是動態代理,依賴於spring-data-redis。
此方案供學習,也提供一種思路讓大家思考。如文中有bug,可以聯系我。如有更好的方案,也聯系我。如覺得不錯想打賞,非常感謝。