Java 缓存
缓存是计算机系统中必不可少的一种解决性能问题的方法,常见的应用包括
- CPU缓存
- 操作系统缓存->减少磁盘机械操作
- 本地缓存
- 分布式缓存
- HTTP缓存->减少应用服务器请求
- 数据库缓存->减少文件系统I/O
其核心就是用空间换时间,通过分配一块高速存储区域(一般来说是内存)来提高数据的读写效率,实现的难点就在于清空策略的实现,比较合理的思路就是定时回收与即时判断数据是否过期相结合。
1、相关概念
- 外存:除计算机内存及CPU缓存以外的储存器
此类储存器一般断电后仍然能保存数据。
常见的外存储器有硬盘、软盘、光盘、U盘等,一般的软件都是安装在外存中。 - 内存:用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据
是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。内存(Memory)也被称为内存储器。
只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行,此类储存器一般断电后数据就会被清空。
2、Java缓存
缓存就是把一些外存上的数据保存到内存上。
我们运行的所有程序,里面的变量值都是放在内存上的,所以说如果要想使一个值放到内存上,实质就是在获得这个变量之后,用一个生存期较长的变量存放你想存放的值,在java中一些缓存一般都是通过map集合来做的。
2.1 概念
如果某些资源或者数据会被频繁的使用,而这些资源或数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都从数据库或者硬盘上去获取,速度会很慢,会造成性能问题。
一个简单的解决方法就是:把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,那么就直接使用,如果没有那么就获取它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取了。从而节省大量的时间,当然,缓存是一种典型的空间换时间的方案。
在Java中最常见的一种实现缓存的方式就是使用Map, 基本的步骤是:
1.先到缓存里面查找,看看是否存在需要使用的数据
2.如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置回到缓存中,以备下次使用
3.如果找到了相应的数据,或者是创建了相应的数据,那就直接使用这个数据。
2.2 示例
/**
* Java中缓存的基本实现示例
*/
public class JavaCache {
/**
* 缓存数据的容器,定义成Map是方便访问,直接根据Key就可以获取Value了
* key选用String是为了简单,方便演示
*/
private Map<String,Object> map = new HashMap<String,Object>();
/**
* 从缓存中获取值
* @param key 设置时候的key值
* @return key对应的Value值
*/
public Object getValue(String key){
//先从缓存里面取值
Object obj = map.get(key);
//判断缓存里面是否有值
if(obj == null){
//如果没有,那么就去获取相应的数据,比如读取数据库或者文件
//这里只是演示,所以直接写个假的值
obj = key+",value";
//把获取的值设置回到缓存里面
map.put(key, obj);
}
//如果有值了,就直接返回使用
return obj;
}
}
这里只是缓存的基本实现,还有很多功能都没有考虑,比如缓存的清除,缓存的同步等等。
下面用单例模式实现缓存:
/**
* 使用缓存来模拟实现单例
*/
public class Singleton {
/**
* 定义一个缺省的key值,用来标识在缓存中的存放
*/
private final static String DEFAULT_KEY = "One";
/**
* 缓存实例的容器
*/
private static Map<String,Singleton> map = new HashMap<String,Singleton>();
/**
* 私有化构造方法
*/
private Singleton(){
//
}
public static Singleton getInstance(){
//先从缓存中获取
Singleton instance = (Singleton)map.get(DEFAULT_KEY);
//如果没有,就新建一个,然后设置回缓存中
if(instance==null){
instance = new Singleton();
map.put(DEFAULT_KEY, instance);
}
//如果有就直接使用
return instance;
}
}
3、Local缓存
Java的本地缓存很早就有了相关标准javax.cache,要求的特性包括原子操作、缓存读写、缓存事件监听器、数据统计等内容。实际工作中本地缓存主要用于特别频繁的稳定数据,不然的话带来的数据不一致会得不偿失。实践中,常使用Guava Cache,以及与Spring结合良好的EhCache
3.1 Guava Cache
是一个全内存的本地缓存实现,它提供了线程安全的实现机制,简单易用,性能好。其创建方式包括cacheLoader和callable callback两种,前者针对整个cache,而后者比较灵活可以在get时指定。
CacheBuilder.newBuilder();//方法创建cache时重要的几个方法如下所示:
- maximumSize(long):设置容量大小,超过就开始回收。
- expireAfterAccess(long, TimeUnit):在这个时间段内没有被读/写访问,就会被回收。
- expireAfterWrite(long, TimeUnit):在这个时间段内没有被写访问,就会被回收 。
- removalListener(RemovalListener):监听事件,在元素被删除时,进行监听。
示例:
@Service
public class ConfigCenterServiceImpl implements ConfigCenterService {
private final static long maximumSize = 20;
/**
* 最大20个,过期时间为1天
*/
private Cache<String, Map<String, ConfigAppSettingDto>> cache = CacheBuilder.newBuilder().maximumSize(maximumSize)
.expireAfterWrite(1, TimeUnit.DAYS).build();
@Autowired
private ConfigAppSettingDAO configAppSettingDAO;
@Override
public ConfigAppSettingDto getByTypeNameAndKey(String configType, String appID, String key) {
Map<String, ConfigAppSettingDto> map = getByType(configType, appID);
return map.get(key);
}
/************************** 辅助方法 ******************************/
private Map<String, ConfigAppSettingDto> getByType(String configType, String appID) {
try {
return cache.get(configType, new Callable<Map<String, ConfigAppSettingDto>>() {
@Override
public Map<String, ConfigAppSettingDto> call() throws Exception {
Map<String, ConfigAppSettingDto> result = Maps.newConcurrentMap();
List<ConfigAppSetting> list = configAppSettingDAO.getByTypeName(configType, appID);
if (null != list && !list.isEmpty()) {
for (ConfigAppSetting item : list) {
result.put(item.getAppkey(), new ConfigAppSettingDto(item.getAppkey(), item.getAppvalue(),
item.getDescription()));
}
}
return result;
}
});
} catch (ExecutionException ex) {
throw new BizException(300, "获取ConfigAppSetting配置信息失败");
}
}
}
3.2 EhCache
官方文档:http://www.ehcache.org/documentation/
EHCache也是一个全内存的本地缓存实现,符合javax.cache JSR-107规范,被应用在Hibernate中,过去存在过期失效的缓存元素无法被GC掉,造成内存泄露的问题,其主要类型及使用示例如下所示。
- Element:缓存的元素,它维护着一个键值对。
- Cache:它是Ehcache的核心类,它有多个Element,并被CacheManager管理,实现了对缓存的逻辑操作行为。
- CacheManager:Cache的容器对象,并管理着Cache的生命周期。
Spring Boot整合Ehcache示例:
//maven配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
//开启Cache
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
//方式1
@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {
@Cacheable
User findByName(String name);
}
//方式2
@Service
public class CacheUserServiceImpl implements CacheUserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> getUsers() {
return userMapper.findAll();
}
// Cacheable表示获取缓存,内容会存储在people中,包含两个Key-Value
@Override
@Cacheable(value = "people", key = "#name")
public User getUser(String name) {
return userMapper.findUserByName(name);
}
//put是存储
@CachePut(value = "people", key = "#user.userid")
public User save(User user) {
User finalUser = userMapper.insert(user);
return finalUser;
}
//Evict是删除
@CacheEvict(value = "people")
public void remove(Long id) {
userMapper.delete(id);
}
}
//在application.properties指定spring.cache.type=ehcache即可
//在src/main/resources中创建ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<cache name="users"
maxEntriesLocalHeap="100"
timeToLiveSeconds="1200">
</cache>
</ehcache>
4、Remote缓存
常见的分布式缓存组件包括memcached,redis等。
-
memcached
性能高效,使用方便,但功能相对单一,只支持字符串类型的数据,需要结合序列化协议,只能用作缓存。 -
Redis
目前最流行的缓存服务器,具有高效的存取速度,高并发的吞吐量,并且有丰富的数据类型,支持持久化。因此,应用场景非常多,包括数据缓存、分布式队列、分布式锁、消息中间件等。
Redis支持更丰富的数据结构, 例如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。
此外,Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
可以说Redis兼具了缓存系统和数据库的一些特性,因此有着丰富的应用场景。本文介绍Redis在Spring Boot中两个典型的应用场景。
Spring Redis默认使用JDK进行序列化和反序列化,因此被缓存对象需要实现java.io.Serializable接口,否则缓存出错。
4.1 数据缓存
//application.properties配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
//方法1
@Configuration
@EnableCaching
public class CacheConfig {
@Autowired
private JedisConnectionFactory jedisConnectionFactory;
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate());
return redisCacheManager;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
// 开启事务支持
redisTemplate.setEnableTransactionSupport(true);
// 使用String格式序列化缓存键
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
return redisTemplate;
}
}
//方法2,和之前Ehcache方式一致
4.2 共享session
//Session配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*)//
public class SessionConfig {
}
//示例
@RequestMapping("/session")
@RestController
public class SessionController {
@Autowired
private UserRepository userRepository;
@RequestMapping("/user/{id}")
public User getUser(@PathVariable Long id, HttpSession session) {
User user = (User) session.getAttribute("user" + id);
if (null == user) {
user = userRepository.findOne(id);
session.setAttribute("user" + id, user);
}
return user;
}
}
参考文档: