在分布式 web 项目中,通常即需要本地缓存提高程序性能,也需要共享缓存在多机之间交换数据。本文介绍了使用Spring缓存抽象机制混合部署的方法。
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,使用Spring缓存抽象,程序员仅需要使用少量的注释就实现了对象的缓存,不必了解复杂的面向对象的编程(AOP)技术。
Spring 的缓存技术相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition, 而且支持 Ehcache 2.x, Gemfire cache, Caffeine, Guava caches 以及 JSR-107 兼容的缓存 (例如 Ehcache 3.x)产品。
本文第一部分简单介绍注释驱动的缓存技术,第二部分介绍使用 Redis 做共享缓存,第三部分介绍混合部署。
这里假设你对Java缓存有一些了解,这里主要介绍 Spring4,EhCache3 的配置方法。其他配置请参考 Spring官网(英文)文档:
配置类如下:
@Configuration
@EnableCaching
public class CacheConfig {
// http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html#cache-store-configuration-jsr107
//http://stackoverflow.com/questions/39386830/using-ehcache-3-with-spring-annotations-not-using-spring-boot
@Bean
public JCacheCacheManager jcacheCacheManager(){
JCacheCacheManager cm = new JCacheCacheManager();
cm.setCacheManager(jsr107cacheManager());
return cm;
}
@Bean
public CacheManager jsr107cacheManager(){
//http://www.ehcache.org/documentation/3.1/107.html
CachingProvider provider = Caching.getCachingProvider();
CacheManager cacheManager = provider.getCacheManager();
MutableConfiguration configuration =
new MutableConfiguration()
// Cannot set type for store! this may be a bug in spring or ehCache
//.setTypes(Long.class, String.class)
.setStoreByValue(false)
.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_MINUTE));
cacheManager.createCache("foo", configuration);
return cacheManager;
}
}
@Configuration 表示这是一个配置类
@EnableCaching 启动 Cache 注释
第一个 Bean 声明 Spring Cache 的 CacheManager 抽象管理接口对象。该对象是 JSR107 缓存规范的一个适配器(Adapter)
第二个 Bean 是 JSR107规范缓存的管理对象。Caching.getCachingProvider() 能正确找到注册的具体实现模块,如 EhCache3。然后创建了 “foo” 缓存区域。
在程序启动时:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(PersistenceJDBCConfig.class);
ctx.register(CacheConfig.class);
ctx.register(RedisConfig.class);
ctx.refresh();
如果你不了解 Spring 配置类的加载,建议阅读: 使用 Java 配置进行 Spring bean 管理
org.springframework
spring-context-support
${springframework.version}
org.ehcache
ehcache
3.1.3
javax.cache
cache-api
1.0.0
注意:必须添加 ehcache3 让 JSR107 模块找到实现。
@Service
@Transactional
public class CacheService {
@Cacheable("foo")
public Foo findBook(String name) {
System.out.print("cache....or not?\n");
return new Foo("jdbc test1");
}
}
假设你需要缓存结果,仅需要在@Service对象的方法上注释 @Cachable("foo") 即可实现该业务对象的缓存。
除 @Cacheable 外,常用的还有 @CachePut、@CacheEvict 注释,具体使用见:注释驱动的 Spring cache 缓存介绍 这些内容在新版本中没有变化。
注意:
(1)注意缓存与交易的关系。建议缓存在服务以上层使用,在 数据访问层(DAO)中谨慎使用!!!
(2)如果你喜欢JSR的注释,请参考Spring官方文档(英文)。
Redis 的中文介绍:http://www.redis.cn/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
// Defaults
redisConnectionFactory.setHostName("127.0.0.1");
redisConnectionFactory.setPort(6379);
return redisConnectionFactory;
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory rf) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(rf);
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
// Number of seconds before expiration. Defaults to unlimited (0)
cacheManager.setDefaultExpiration(10); // Sets the default expire time (in seconds)
return cacheManager;
}
}
注意:设置 Cache 过期时间要合适,太长就长期有效,太短你看不到测试结果。建议 10-30秒。
org.springframework.data
spring-data-redis
1.6.0.RELEASE
redis.clients
jedis
2.7.3
org.apache.commons
commons-pool2
2.4.2
其中,commons-pool2 是 访问 Redis 服务器的链接池需要的依赖。
具体代码见:Redis 缓存 + Spring 的集成示例 ,作者写的很不错。
通过前面的配置,我们发现 Spring 缓存抽象 就是在其他 Cache 管理程序基础上提出了自己的抽象体系,即利用适配器模式建立了通用的 Cache 管理体系。
找到 org.springframework.cache.CacheManager API文档,发现改接口相当简单。我们需要做的工作就是将两个 Manager 组合一下。 代码:
MixCacheManager 源代码:
public class MixCacheManager implements CacheManager {
private CacheManager redisCacheManager;
private CacheManager memCacheManager;
private String redisPrefix = "redis-";
public Cache getCache(String arg0) {
if (arg0.startsWith(redisPrefix))
return redisCacheManager.getCache(arg0);
else
return memCacheManager.getCache(arg0);
}
public Collection getCacheNames() {
Collection cacheNames = new ArrayList();
if (redisCacheManager != null) {
cacheNames.addAll(redisCacheManager.getCacheNames());
}
if (memCacheManager != null) {
cacheNames.addAll(memCacheManager.getCacheNames());
}
return cacheNames;
}
// getting & setting ...
}
该类非常简单。注意:redisCacheManager 和 memCacheManager 都是实现了 org.springframework.cache.CacheManager 接口的类。MixCacheManager也实现了该接口。
getCacheNames 的任务就是拼接所以的 Cache 区域;
getCache 就是根据应用需要的 Cache 名字,返回合适的 Cache。这里指定“redis-”开头的名字,使用共享缓存。
修改 RedisConfig 类,添加
// Problem with Autowiring & No unique bean
// http://stackoverflow.com/questions/2699608/problem-with-autowiring-no-unique-bean
@Bean
@Primary
public CacheManager cacheManager(RedisCacheManager redisCacheManager,JCacheCacheManager jcacheCacheManager) {
MixCacheManager cacheManager = new MixCacheManager();
cacheManager.setRedisCacheManager(redisCacheManager);
cacheManager.setMemCacheManager(jcacheCacheManager);
return cacheManager;
}
@Primary 是让 Spring Cache 知道,现在 org.springframework.cache.CacheManager 三个实现中,你该使用混合实现。
测试使用的服务代码:
@Service
@Transactional
public class CacheService {
@Cacheable("foo")
public Foo findBook(String name) {
System.out.print("cache....or not?\n");
return new Foo("jdbc test1");
}
@Cacheable("redis-foo")
public Foo findBookRedis(String name) {
System.out.print("cache Redis....or not?\n");
return new Foo("jdbc test1");
}
}
测试的使用代码:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(PersistenceJDBCConfig.class);
ctx.register(CacheConfig.class);
ctx.register(RedisConfig.class);
ctx.refresh();
CacheService cs = ctx.getBean(CacheService.class);
cs.findBook("naan1");
cs.findBook("naan1");
cs.findBook("naan1");
cs.findBook("new1");
cs.findBookRedis("redis");
cs.findBookRedis("redis");
cs.findBookRedis("redis");
cs.findBookRedis("redis-new");
输出结果:
cache....or not?
cache....or not?
cache Redis....or not?
cache Redis....or not?
在 Redis 控制台上输入 keys * 看到建立的 key。
总结:
(1)Redis 与 内存缓存 混合使用非常简单实用。
(2)设计模式就是 Java 的魂。了解设计模式,就能看代码猜出有趣的实现。