利用redis做业务缓存和配置项

2018-01-05 10:25:37来源:https://segmentfault.com/a/1190000012709271作者:SegmentFault人点击

分享
利用redis做业务缓存和配置项

来自: https://github.com/018/RedisC...


背景

​ 从以前的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源代码,发现其核心代码就是动态代理。那什么是动态代理呢?我就不详细解释了,对它不了解的还是乖乖的 百度一下动态代理 。这里从网上投了一张图,如下:


%20
%20

它大概就是我们可以动态的自定义的控制实现。给你Object%20proxy、Method%20method、Object[]%20args三个参数,然后你自己决定怎么实现。给个简单的例子:

%20
%20/**
*%20接口
*/
public%20interface%20Something%20{
String%20get(String%20key);
String%20set(String%20key,%20String%20value);
}
/**
%20*%20调用处理器
%20*/
public%20class%20MyInvocationHandler%20implements%20InvocationHandler%20{
@Override
public%20Object%20invoke(Object%20proxy,%20Method%20method,%20Object[]%20args)%20throws%20Throwable%20{
System.out.println(method.getName()%20+%20"%20doing%20...");
System.out.println("proxy%20is%20"%20+%20proxy.getClass().getName());
System.out.println("method%20is%20"%20+%20method.getName());
System.out.println("args%20is%20"%20+%20Arrays.toString(args));
System.out.println(method.getName()%20+%20"%20done!");
return%20method.getName()%20+%20"%20invoked";
}
}
public%20class%20Test%20{
public%20static%20void%20main(String[]%20args)%20{
Something%20somethingProxy%20=%20(Something)%20java.lang.reflect.Proxy.newProxyInstance(Something.class.getClassLoader(),
new%20Class<?>[]{Something.class},
new%20MyInvocationHandler());
System.out.println("somethingProxy.get(/"name/"):%20"%20+%20somethingProxy.get("name"));
System.out.println();
System.out.println("somethingProxy.set(/"name/",%20/"018/"):%20"%20+%20somethingProxy.set("name",%20"018"));
System.out.println();
}
}%20
%20

以上代码的输出结果:

%20
%20get%20doing%20...
proxy%20is%20com.sun.proxy.$Proxy0
method%20is%20get
args%20is%20[name]
get%20done!
somethingProxy.get("name"):%20get%20invoked
set%20doing%20...
proxy%20is%20com.sun.proxy.$Proxy0
method%20is%20set
args%20is%20[name,%20018]
set%20done!
somethingProxy.set("name",%20"018"):%20set%20invoked%20
通过Proxy.newProxyInstance创建一个Something的代理对象somethingProxy。%20
通过somethingProxy实例调用其方法get/set时,会执行MyInvocationHandler.invoke方法。%20
%20思考%20
%20

​%20缓存,通过Key,返回值。

%20
%20

​%20动态代理,通过Method(方法),执行返回值。

%20
%20

%20​%20怎么把它们关联起来呢?方法有方法名,那能不能把%20Method%20method%20的方法名对应到Key?%20能!!!%20

%20
%20方案%20
%20

​%20在最开始的例子获取ip就应该这样写:

%20
%20public%20interface%20DataSourceSettings%20{
String%20getIp();
void%20setIp(String%20ip);
int%20getPort();
void%20setPort(int%20port);
//%20其他项%20...
}
public%20class%20SettingsInvocationHandler%20implements%20InvocationHandler%20{
@Override
public%20Object%20invoke(Object%20proxy,%20Method%20method,%20Object[]%20args)%20throws%20Throwable%20{
String%20methodName%20=%20method.getName();
//%20TODO:%20
//%201、去掉get/set,截图后面的字符串作为Key
//%202、redis客户端通过Key获取值返回
}
}%20
%20

配置项完美解决了。

%20
%20

%20​%20但业务缓存呢?相对来说,配置项的Key是固定的,而业务缓存的Key则是不固定的。比如缓存商品信息,商品id为001和002等等,得缓存不同的Key。就得有一个动态Key的解决方案,即%20productCaches.put(商品id,%20商品实体)%20这样的实现方式。%20

%20
%20

​%20参考spring-data-redis的BoundHashOperations,可以对其进行扩展实现这一功能。这样我们就可以这样定义一个商品缓存接口:

%20
%20public%20interface%20HashSessionOperations<HK,%20HV>%20extends%20BoundHashOperations<String,%20HK,%20HV>%20{
}
public%20interface%20ProductCaches%20extends%20HashSessionOperations%20{
}%20
%20

缓存数据和获取数据则如下:

%20
%20productCaches.put(product1.prod_id,%20product1);//缓存数据
Product%20product%20=%20(Product)productCaches.get(prod_id);//获取缓存数据%20
%20

至此,业务缓存也完美解决。

%20
%20

​%20当然,我们对BoundListOperations、BoundSetOperations、BoundValueOperations、BoundZSetOperations进行对应的扩展。这样,这些不仅仅做业务缓存,也可以用它来作为redis的一个客户端使用。

%20
%20

​%20看到这里,只看到了接口,也即是结果,知道了怎么使用它应用到项目中。但,怎么实现的呢?但是是动态代理。来,废话不多说,两个InvocationHandler码上来:

%20
%20/**
%20*%20简单的InvocationHandler
%20*%20主要用于执行配置项
%20*/
public%20class%20SimpleSessionOperationInvocationHandler%20implements%20InvocationHandler%20{
private%20static%20final%20String%20METHOD_SET%20=%20"set";
private%20static%20final%20String%20METHOD_GET%20=%20"get";
private%20static%20final%20String%20METHOD_TOSTRING%20=%20"toString";
private%20DefaultSimpleSessionOperations%20defaultSimpleSessionOperations;
public%20SimpleSessionOperationInvocationHandler(DefaultSimpleSessionOperations%20defaultSimpleSessionOperations)%20{
this.defaultSimpleSessionOperations%20=%20defaultSimpleSessionOperations;
}
@Override
public%20Object%20invoke(Object%20proxy,%20Method%20method,%20Object[]%20args)%20throws%20Throwable%20{
Class<?>%20cls%20=%20method.getDeclaringClass();
String%20group%20=%20cls.getSimpleName().replace("Settings",%20"").replace("Setting",%20"");
String%20methodName%20=%20method.getName();
String%20methodString%20=%20null;
String%20item%20=%20null;
Object%20value%20=%20null;
if%20(METHOD_TOSTRING.equals(methodName))%20{
return%20cls.getName();
}%20else%20if%20(METHOD_SET.equals(methodName))%20{
//%20void%20set(String%20item,%20?%20value)
if%20(method.getParameterCount()%20!=%202%20||%20!method.getParameterTypes()[0].getSimpleName().equals(String.class.getSimpleName())%20||
!method.getParameterTypes()[1].getSimpleName().equals(Object.class.getSimpleName())%20||
args%20==%20null%20||%20args.length%20!=%202)%20{
throw%20new%20NonsupportMethodException(cls.getPackage().getName(),%20cls.getSimpleName(),%20methodName,
"方法声明错误,正确为%20void%20set(String%20key,%20?%20value)。");
}
methodString%20=%20METHOD_SET;
item%20=%20args[0].toString();
value%20=%20args[1];
}%20else%20if%20(METHOD_GET.equals(methodName))%20{
//%20?%20get(String%20item)
if%20(method.getParameterCount()%20!=%201%20||%20!method.getParameterTypes()[0].getSimpleName().equals(String.class.getSimpleName())%20||
args%20==%20null%20||%20args.length%20!=%201)%20{
throw%20new%20NonsupportMethodException(cls.getPackage().getName(),%20cls.getSimpleName(),%20methodName,
"方法声明错误,正确为%20?%20get(String%20item)。");
}
methodString%20=%20METHOD_GET;
item%20=%20args[0].toString();
}%20else%20if%20(methodName.startsWith(METHOD_SET))%20{
//%20void%20setXXX(?%20value)
if%20(method.getParameterCount()%20!=%201%20||
args%20==%20null%20||%20args.length%20!=%201)%20{
throw%20new%20NonsupportMethodException(cls.getPackage().getName(),%20cls.getSimpleName(),%20methodName,
"方法声明错误,正确为%20void%20setXXX(?%20value)。");
}
methodString%20=%20METHOD_SET;
item%20=%20methodName.substring(METHOD_SET.length());
value%20=%20args[0];
}%20else%20if%20(methodName.startsWith(METHOD_GET))%20{
//%20Object%20getXXX()
if%20(method.getParameterCount()%20!=%200%20||
(args%20!=%20null%20&&%20args.length%20!=%200))%20{
throw%20new%20NonsupportMethodException(cls.getPackage().getName(),%20cls.getSimpleName(),%20methodName,
"方法声明错误,正确为%20Object%20getXXX()。");
}
methodString%20=%20METHOD_GET;
item%20=%20methodName.substring(METHOD_GET.length());
}%20else%20{
throw%20new%20NonsupportMethodException(cls.getPackage().getName(),%20cls.getSimpleName(),%20methodName,
"不支持的方法,只能是void%20set(String%20item,%20?%20value)、?%20get(String%20item)、void%20setXXX(?%20value)、?%20getXXX()。");
}
switch%20(methodString)%20{
case%20(METHOD_GET):
Object%20val%20=%20this.defaultSimpleSessionOperations.get(group,%20item);
return%20val;
case%20(METHOD_SET):
this.defaultSimpleSessionOperations.put(group,%20item,%20value);
}
return%20null;
}
}%20
%20/**
%20*%20redis操作动态代理执行类
%20*%20主要用于执行业务缓存
%20*/
public%20class%20RedisSessionOperationInvocationHandler%20implements%20InvocationHandler%20{
private%20static%20final%20String%20METHOD_TOSTRING%20=%20"toString";
BoundKeyOperations<?>%20sessionOperations;%20//%20具体执行的redis对象
public%20RedisSessionOperationInvocationHandler(BoundKeyOperations<?>%20sessionOperations)%20{
this.sessionOperations%20=%20sessionOperations;
}
@Override
public%20Object%20invoke(Object%20proxy,%20Method%20method,%20Object[]%20args)%20throws%20Throwable%20{
Class<?>%20cls%20=%20method.getDeclaringClass();
String%20methodName%20=%20method.getName();
Class<?>%20targetCls%20=%20sessionOperations.getClass();
Method%20methodTarget%20=%20targetCls.getDeclaredMethod(methodName,%20method.getParameterTypes());
if%20(methodTarget%20==%20null)%20{
throw%20new%20NonsupportMethodException(cls.getPackage().getName(),%20cls.getSimpleName(),%20methodName,
"不支持"%20+%20methodName%20+%20"方法。");
}
if%20(METHOD_TOSTRING.equals(methodName))%20{
return%20cls.getName();
}
Object%20result%20=%20methodTarget.invoke(sessionOperations,%20args);
return%20result;
}
}%20
%20

至于接口创建代理,就交给ClassScannerConfigurer吧。

%20
%20/**
%20*%20类扫描配置类
%20*/
public%20class%20ClassScannerConfigurer%20implements%20InitializingBean,%20ApplicationContextAware,%20BeanFactoryAware,%20BeanNameAware%20{
/**
%20*%20待扫描的包
%20*/
private%20String%20basePackage;
private%20String%20beanName;
private%20DefaultListableBeanFactory%20beanFactory;
private%20ApplicationContext%20applicationContext;
private%20SessionOperationsFactory%20sessionOperationsFactory;
private%20List<ClassInfo>%20classInfos;
public%20String%20getBasePackage()%20{
return%20basePackage;
}
public%20void%20setBasePackage(String%20basePackage)%20{
this.basePackage%20=%20basePackage;
}
public%20SessionOperationsFactory%20getSessionOperationsFactory()%20{
return%20sessionOperationsFactory;
}
public%20void%20setSessionOperationsFactory(SessionOperationsFactory%20sessionOperationsFactory)%20{
this.sessionOperationsFactory%20=%20sessionOperationsFactory;
}
@Override
public%20void%20setBeanName(String%20name)%20{
this.beanName%20=%20name;
}
@Override
public%20void%20afterPropertiesSet()%20throws%20Exception%20{
Assert.notNull(this.basePackage,%20"Property%20'basePackage'%20is%20required");
//%20扫描并创建接口的动态代理
ClassPathScanner%20scanner%20=%20new%20ClassPathScanner();
scanner.setResourceLoader(this.applicationContext);
Set<Class>%20classes%20=%20scanner.doScans(StringUtils.tokenizeToStringArray(this.basePackage,%20ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
if%20(Objects.isNull(classInfos))%20{
classInfos%20=%20new%20ArrayList<>(classes.size());
}
for%20(Class<?>%20cls%20:%20classes)%20{
Object%20proxyObject%20=%20ProxyManager.newProxyInstance(this.sessionOperationsFactory,%20cls);
String%20beanName%20=%20cls.getSimpleName().substring(0,%201).toLowerCase()%20+%20cls.getSimpleName().substring(1);
this.beanFactory.registerSingleton(beanName,%20proxyObject);
ClassInfo%20classInfo%20=%20new%20ClassInfo(beanName,%20cls,%20proxyObject);
classInfos.add(classInfo);
}
}
@Override
public%20void%20setApplicationContext(ApplicationContext%20applicationContext)%20throws%20BeansException%20{
this.applicationContext%20=%20applicationContext;
}
@Override
public%20void%20setBeanFactory(BeanFactory%20beanFactory)%20throws%20BeansException%20{
this.beanFactory%20=%20(DefaultListableBeanFactory)%20beanFactory;
}
}%20
%20

怎么加载这些呢,交给spring吧。

%20
%20<?xml%20version="1.0"%20encoding="UTF-8"?>
<beans%20xmlns="http://www.springframework.org/schema/beans"
%20xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
%20xsi:schemaLocation="
%20http://www.springframework.org/schema/beans%20http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--Redis%20线程池配置%20-->
<bean%20id="jpoolConfig"%20class="redis.clients.jedis.JedisPoolConfig">
<property%20name="maxIdle"%20value="200"></property>
<property%20name="testOnBorrow"%20value="true"></property>
</bean>
<!--连接工厂%20-->
<bean%20id="connectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property%20name="hostName"%20value="127.0.0.1"></property>
<property%20name="port"%20value="6339"></property>
<property%20name="usePool"%20value="true"></property>
<property%20name="timeout"%20value="100000"></property>
<property%20name="poolConfig"%20ref="jpoolConfig"></property>
</bean>
<!--数据模板%20-->
<bean%20id="redisTemplate"%20class="org.springframework.data.redis.core.RedisTemplate">
<property%20name="connectionFactory"%20ref="connectionFactory"></property>
</bean>
<!--redis%20template%20缓存管理%20-->
<bean%20id="cacheManager"%20class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg%20ref="redisTemplate"></constructor-arg>
<property%20name="usePrefix"%20value="true"></property>
<property%20name="loadRemoteCachesOnStartup"%20value="true"></property>
</bean>
<!--%20上面是配置redis的,从这里开始才是%20-->
<!--%20配置settingSessionFactory%20-->
<bean%20id="settingSessionFactory"%20class="com.o18.redis.cache.SessionOperationsFactory">
<constructor-arg%20ref="redisTemplate"/>
<constructor-arg%20value="ORDER"/>
</bean>
<!--%20扫描Setting%20-->
<bean%20class="com.o18.redis.cache.ClassScannerConfigurer">
<property%20name="sessionOperationsFactory"%20ref="settingSessionFactory"%20/>
<property%20name="basePackage"%20value="com.o18.redis.cache.caches.*;com.o18.redis.cache.settings.*"%20/>
</bean>
</beans>%20
%20优点%20
%20代码统一管理%20。一个包是系统配置项com.**.settings.*,一个包是业务缓存com.**.caches.*。
%20配置项随时改随时生效%20。有些调优的参数,有些在特殊时期需要即时调整的。通过用web管理界面,友好的解决。
%20扩展%20
%20

​%20上面提到用web管理界面来即时修改配置项,即可以用一些特性,扫描所有配置项提供修改,分组、排序等等都是可以做到的。

%20
%20

​%20还有,等等...

%20
%20反思%20
%20安全%20。原来配置项安安全全的在properties文件躺着,这样强行把它拉到安全的问题上来,当然祈祷redis安全!
%20默认方法不行%20。接口上有默认方法,那段代码就相对于废了。
%20性能%20。没实际做过压测,但鉴于mybatis,如果出现性能问题,那就是我写的代码需要优化,不是方案问题。
%20总结%20
%20

通过mybatis的动态代理,实现基于redis的配置项即时修改生效。还扩展了业务缓存,使其代码集中。该方案中核心是动态代理,依赖于spring-data-redis。

%20
%20

此方案供学习,也提供一种思路让大家思考。如文中有bug,可以联系我。如有更好的方案,也联系我。如觉得不错想打赏,非常感谢。

%20
%20


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台