缓存介绍
缓存是实际工作中非经常常使用的一种提高性能的方法, 我们会在很多场景下来使用缓存。
本文通过一个简单的样例进行展开,通过对照我们原来的自己定义缓存和 spring 的基于注解的 cache 配置方法,展现了 spring cache 的强大之处,然后介绍了其主要的原理,扩展点和使用场景的限制。通过阅读本文。你应该能够短时间内掌握 spring 带来的强大缓存技术。在非常少的配置下就可以给既有代码提供缓存能力。
Spring 3.1 引入了激动人心的基于凝视(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(比如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中加入少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性。不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存暂时存储方案,也支持和主流的专业缓存比如 EHCache 集成。
其特点总结例如以下:
- 通过少量的配置 annotation 凝视就可以使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件就可以使用缓存
- 支持 Spring Express Language,能使用对象的不论什么属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过事实上现不论什么方法的缓存支持
- 支持自己定义 key 和自己定义缓存管理者,具有相当的灵活性和扩展性
- 本文将针对上述特点对 Spring cache 进行具体的介绍,主要通过一个简单的样例和原理介绍展开,然后我们将一起看一个比較实际的缓存样例。最后会介绍 spring cache 的使用限制和注意事项。
我们自己实现缓存
我们自己实现一个缓存如何实现,比如我们要缓存用户账号信息
package com.wfg.cache.entity;
/**
* spring
*
* @Title: com.wfg.cache.entity
* @Date: 2021/1/2 0002 22:40
* @Author: wfg
* @Description:
* @Version:
*/
public class Account {
private int id;
private String name;
public Account(String name) {
this.name = name;
}
public Account(String name,int id) {
this.name = name;
this.id=id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
提供一个缓存管理器简单存储在ConcurrentHashMap
package com.wfg.cache;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* spring
*
* @Title: com.wfg.cache
* @Date: 2021/1/2 0002 22:41
* @Author: wfg
* @Description:
* @Version:
*/
@Component
public class CacheContext <T> {
private Map<String, T> cache = new ConcurrentHashMap<>();
public T get(String key){
return cache.get(key);
}
public void addOrUpdateCache(String key,T value) {
cache.put(key, value);
}
// 依据 key 来删除缓存中的一条记录
public void evictCache(String key) {
if(cache.containsKey(key)) {
cache.remove(key);
}
}
// 清空缓存中的全部记录
public void evictCache() {
cache.clear();
}
}
我们编写一个服务类
package com.wfg.cache.service;
import com.wfg.cache.CacheContext;
import com.wfg.cache.entity.Account;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
/**
* spring
*
* @Title: com.wfg.cache.service
* @Date: 2021/1/2 0002 22:44
* @Author: wfg
* @Description:
* @Version:
*/
@Service
public class AccountService {
@Resource
private CacheContext<Account> accountCacheContext;
public Account getAccountByName(String accountName) {
Account result = accountCacheContext.get(accountName);
if (result != null) {
System.out.println("从缓存中取到的"+result);
return result;
}
Optional<Account> accountOptional = getFromDB(accountName);
if (!accountOptional.isPresent()) {
throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));
}
Account account = accountOptional.get();
accountCacheContext.addOrUpdateCache(accountName, account);
return account;
}
public void reload() {
accountCacheContext.evictCache();
}
private Optional<Account> getFromDB(String accountName) {
System.out.println(String.format("real querying db... [%s]", accountName));
//Todo query data from database
return Optional.ofNullable(new Account(accountName));
}
}
进行测试:
package com.wfg.cache.test;
import com.wfg.cache.entity.Account;
import com.wfg.cache.service.AccountService;
import com.wfg.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* spring
*
* @Title: com.wfg.cache.test
* @Date: 2021/1/2 0002 22:56
* @Author: wfg
* @Description:
* @Version:
*/
public class TestServer {
public static void main(String[] args) {
AnnotationConfigApplicationContext configApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
AccountService bean = configApplicationContext.getBean(AccountService.class);
System.out.println("===================================================");
Account zhangsan = bean.getAccountByName("zhangsan");
Account zhangsan1 = bean.getAccountByName("zhangsan");
Account zhangsan2 = bean.getAccountByName("zhangsan");
bean.reload();
bean.getAccountByName("zhangsan");
bean.getAccountByName("zhangsan");
bean.getAccountByName("zhangsan");
}
}
可以看出来缓存已经起到效果了,非常简单吧,但是我们发现业务代码里面加入了缓存代码,这样的耦合性太强了,我们如何把缓存代码摘出来,spring给我们提供了技术支持,如何实现的?
spring 缓存是如何实现的
先编写配置类
package com.wfg.cache.config;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import java.util.*;
/**
* spring
*
* @Title: com.wfg.cache.config
* @Date: 2021/1/2 0002 23:17
* @Author: wfg
* @Description:
* @Version:
*/
//开启缓存
//<cache:annotation-driven />
@EnableCaching
@Configuration
@ComponentScan("com.wfg")
public class CacheConfig {
/**
xml方式
* <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
* <property name="caches">
* <set>
* <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
* <property name="name" value="default"/>
* </bean>
* <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
* <property name="name" value="accountCache"/>
* </bean>
* </set>
* </property>
* </bean>
*/
@Bean
public CacheManager simpleCacheManager(){
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
List<Cache > mapCacheSet = new ArrayList<>();
ConcurrentMapCacheFactoryBean bean = new ConcurrentMapCacheFactoryBean();
bean.setName("default");
ConcurrentMapCacheFactoryBean bean2 = new ConcurrentMapCacheFactoryBean();
bean.setName("accountCache");
//ConcurrentMapCacheFactoryBean
ConcurrentMapCache concurrentMapCache = new ConcurrentMapCache("default");
ConcurrentMapCache concurrentMapCache1 = new ConcurrentMapCache("accountCache");
mapCacheSet.add(concurrentMapCache);
mapCacheSet.add(concurrentMapCache1);
// mapCacheSet.add(bean);
// mapCacheSet.add(bean2);
simpleCacheManager.setCaches(mapCacheSet);
return simpleCacheManager;
}
}
这个时候我们就可以基于spring的注解进行编写业务代码了
package com.wfg.cache.service;
import com.wfg.cache.CacheContext;
import com.wfg.cache.entity.Account;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
/**
* spring
*
* @Title: com.wfg.cache.service
* @Date: 2021/1/2 0002 22:44
* @Author: wfg
* @Description:
* @Version:
*/
@Service("accountService2")
public class AccountService2 {
/**
* 添加一个注解@Cacheable 表示使用缓存accoutCache
* @param accountName
* @return
*/
@Cacheable(value = "accountCache",key = "#accountName",condition = "#accountName!='zhangsan'",unless = "#result==null")
public Account getAccountByName(String accountName) {
//原来的缓存业务去掉
Optional<Account> accountOptional = getFromDB(accountName);
if (!accountOptional.isPresent()) {
throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));
}
Account account = accountOptional.get();
return account;
}
/**
* 清空缓存
* @param account
* @return
*/
@CachePut(value = "accountCache",key = "#account.name",unless = "#result==null")
public Account updateAccount(Account account){
System.out.println("清空缓存"+account);
return new Account("lisi",33);
}
@CacheEvict(cacheNames = "accountCache", allEntries = true)
public void reload() {
}
private Optional<Account> getFromDB(String accountName) {
System.out.println(String.format("real querying db... [%s]", accountName));
//Todo query data from database
return Optional.ofNullable(new Account(accountName));
}
}
进行测试:
package com.wfg.cache.test;
import com.wfg.cache.config.CacheConfig;
import com.wfg.cache.entity.Account;
import com.wfg.cache.service.AccountService;
import com.wfg.cache.service.AccountService2;
import com.wfg.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* spring
*
* @Title: com.wfg.cache.test
* @Date: 2021/1/2 0002 22:56
* @Author: wfg
* @Description:
* @Version:
*/
public class TestServer2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext configApplicationContext = new AnnotationConfigApplicationContext(CacheConfig.class);
AccountService2 bean = configApplicationContext.getBean(AccountService2.class);
System.out.println("===================================================");
Account zhangsan = bean.getAccountByName("zhangsan");
Account zhangsan1 = bean.getAccountByName("zhangsan");
Account zhangsan2 = bean.getAccountByName("zhangsan");
System.out.println(zhangsan.hashCode()+"========"+zhangsan.toString());
System.out.println(zhangsan1.hashCode()+"========"+zhangsan1.toString());
System.out.println(zhangsan2.hashCode()+"========"+zhangsan2.toString());
Account lisi = bean.getAccountByName("lisi");
Account lisi1 = bean.getAccountByName("lisi");
Account lisi2 = bean.getAccountByName("lisi");
System.out.println(lisi.hashCode()+"========"+lisi.toString());
System.out.println(lisi1.hashCode()+"========"+lisi1.toString());
System.out.println(lisi2.hashCode()+"========"+lisi2.toString());
Account lisi3 = bean.updateAccount(new Account("lisi",11));
System.out.println(lisi3.hashCode()+"========"+lisi3.toString());
Account lisi4 = bean.getAccountByName("lisi");
Account lisi5 = bean.getAccountByName("lisi");
System.out.println(lisi4.hashCode()+"========"+lisi4.toString());
System.out.println(lisi5.hashCode()+"========"+lisi5.toString());
}
}
这个配置项缺省使用了一个名字叫 cacheManager 的缓存管理器,这个缓存管理器有一个 spring 的缺省实现,即 org.springframework.cache.support.SimpleCacheManager。这个缓存管理器实现了我们刚刚自己定义的缓存管理器的逻辑,它须要配置一个属性 caches,即此缓存管理器管理的缓存集合,除了缺省的名字叫 default 的缓存,我们还自己定义了一个名字叫 accountCache 的缓存,使用了缺省的内存存储方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一个内存缓存实现方案。
从结果中可以看到,spring的缓存起作用了,并且张三没有进行缓存,李四进行缓存了,并且修改后缓存也进行了更新
我们发现spring也是简单的使用的ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);进行缓存
如果我们进行分布式缓存,或者缓存数据量非常大,服务重启后我们也想进行缓存又改如何?使用第三方缓存进行持久化解决,
比如redis,ecache等等
如何使用第三方缓存
spring-redis
我看市面是配置来了一大堆,我不太喜欢,比较喜欢配置有用的核心配置,也许你拿来不能直接使用
其实说白了就是我们实现Cache接口或者集成AbstractValueAdaptingCache然后把redis客户端注入进来就可以了
这个就是spring缓存的架构图,
由此可见把我们自己的类交给SimpleCacheManger即可所以有了下图的配置:当然你使用注解的方式一样也是
<bean id="jdJimDbCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="com.jd.cjg.demand.util.JdJimDbCache">
<property name="jimClient" ref="jimClient"/>
<property name="name" value="default"/>
<property name="expire" value="1800000"/> <!--30分钟,单位毫秒-->
</bean>
<bean class="com.jd.cjg.demand.util.JdJimDbCache">
<property name="jimClient" ref="jimClient"/>
<property name="name" value="contentCache"/>
<property name="expire" value="86400000"/> <!--24H,单位毫秒-->
</bean>
</set>
</property>
</bean>
<!--jd jimDb redis 实现 的缓存 begin -->
<bean id="jimClient" class="com.jd.jim.cli.ReloadableJimClientFactoryBean">
<property name="jimUrl" value="${jd.jimDb.jimUrl}"/>
</bean>
实现类如下:其中代码非常简单就不再赘述了
@Slf4j
public class JdJimDbCache implements Cache, InitializingBean {
@Resource(name = "jimClient")
private Cluster jimClient;
private String name;
private Long expire; //缓存保存时间
public void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
public Cluster getJimClient() {
return jimClient;
}
public void setJimClient(Cluster jimClient) {
this.jimClient = jimClient;
}
public long getExpire() {
return expire;
}
public void setExpire(long expire) {
this.expire = expire;
}
@Override
public Object getNativeCache() {
return this.jimClient;
}
@Override
public void afterPropertiesSet() throws Exception {
//检查 jimClient 是否已经注入
Assert.notNull(jimClient, "jimDb注入成功!");
}
public String getByKey(String key) {
return jimClient.get(key);
}
private static byte[] objectToByte(Object object) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
return bos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("serialize object error", e);
}
}
@Override
public ValueWrapper get(Object key) {
final String keyf = key.toString();
Object object = jimClient.getObject(keyf);
return (object != null ? new SimpleValueWrapper(object) : null);
}
@Override
public void put(Object key, Object value) {
final String keyf = key.toString();
long liveTime = 30 * 60 * 1000;
if (expire != null && expire != 0) {
liveTime = expire;
}
final Object valuef = value;
//byte[] valueb = toByteArray(valuef);
jimClient.setObject(keyf, valuef);
jimClient.pExpire(keyf, liveTime, TimeUnit.MILLISECONDS);
}
@Override
public void evict(Object key) {
final String keyf = key.toString();
jimClient.del(keyf);
}
@Override
public void clear() {
// jimClient.flushDB();
//throw new UnsupportedOperationException("invoke spring cache clear method not supported");
}
/**
* 描述 : <Object转byte[]>. <br>
* <p>
* <使用方法说明>
* </p>
*
* @param obj
* @return
*/
private byte[] toByteArray(Object obj) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
bytes = bos.toByteArray();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
// @Override
public <T> T get(Object o, Class<T> aClass) {
return null;
}
// @Override
public <T> T get(Object o, Callable<T> callable) {
return null;
}
// @Override
public ValueWrapper putIfAbsent(Object o, Object o1) {
return null;
}
}
客户端调用:
Cache contentCache = jdJimDbCacheManager.getCache("contentCache");
Cache.ValueWrapper valueWrapper = contentCache.get(key + _ERP_INFO);