cache缓存方案
本文目的:囊括介绍市面通行的缓存方案,为选型做准备。具体集成在文末有推介阅读
缓存是什么?缓存是为了减轻数据库的压力而存在。
比如查询某个数据时,首先从缓存中查找,缓存中没有,才会从数据库中查询,也就一定程度减轻了数据库的压力
/**
* 这个例子仅仅作为演示,市面上的缓存框架通常都提供通过注解的方式来创建/获取/更新缓存的数据,很方便
**/
puulic UserEntity getUserInfo(){
// 第一级本地缓存: 从缓存查询
UserEntity user = CacheService.use('user','getUser','123456')
if(user==null){
// 第二级远程缓存: 从Redis查询
user = redisService.get("userId","123456")
if(user==null){
// 缓存中都没数据: 从数据库查询
user = UserService.getByid("123456")
}
}
return user;
}
缓存又分为两种,JVM虚拟机(本地)缓存和远程缓存.其实就是根据
数据存储位置
进行分类。
本地缓存
- 根据HashMap自实现本地缓存
- Guava Cache
- Caffeine
- Encache
- Alibaba JetCache
- memcached
- Layering Cache 框架
远程缓存
- Redis
springboot 中默认提供了以通用的缓存接口,因为JAVA语言缓存是有协议规范的,市面上的缓存框架都是根据通行协议编写,所以spring中可以很方便切换具体的缓存实现。
通常的缓存方案都是: 本地缓存 + Redis(远程)缓存 +数据库
介绍一下:本地缓存
本地缓存仅仅介绍一具自实现的具体实现访问,其他方案仅介绍特性:
- 根据HashMap自实现本地缓存
- Guava Cache
- Caffeine
- Encache
- JetCache
- 根据HashMap自定义实现本地缓存
缓存的本质就是存储在内存中的KV数据结构,对应的就是jdk中的HashMap,但是要实现缓存,还需要考虑并发安全性、容量限制等策略,下面简单介绍一种利用LinkedHashMap实现缓存的方式。
LinkedHashMap维持了一个链表结构,用来存储节点的插入顺序或者访问顺序(二选一),并且内部封装了一些业务逻辑,只需要覆盖removeEldestEntry方法,便可以实现缓存的LRU淘汰策略。
此外我们利用读写锁,保障缓存的并发安全性。需要注意的是,这个示例并不支持过期时间淘汰的策略。
自实现缓存的方式,优点是实现简单,不需要引入第三方包,比较适合一些简单的业务场景。对于比较复杂的场景,建议使用比较稳定的开源工具。
public class LRUCache extends LinkedHashMap {
/**
* 可重入读写锁,保证并发读写安全性
*/
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
/**
* 缓存大小限制
*/
private int maxSize;
public LRUCache(int maxSize) {
super(maxSize + 1, 1.0f, true);
this.maxSize = maxSize;
}
@Override
public Object get(Object key) {
readLock.lock();
try {
return super.get(key);
} finally {
readLock.unlock();
}
}
@Override
public Object put(Object key, Object value) {
writeLock.lock();
try {
return super.put(key, value);
} finally {
writeLock.unlock();
}
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return this.size() > maxSize;
}
}
- Guava Cache
基于Guava Cache实现本地缓存。Guava是Google团队开源的一款 Java 核心增强库
特性:
- 支持最大容量限制
- 支持两种过期删除策略(插入时间和访问时间)
- 支持简单的统计功能
- 基于LRU算法实现
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
Caffeine是以 GuavaCache 为原型而开发的一个本地缓存框架, 相对GuavaCache, 它有更高的性能与命中率, 更强大的功能, 更灵活的配置方式
特性:
- 具备GuavaCache几乎所有特性, 并提供了适配器, 方便Guava用户迁移至Caffeine, 两者差异见: https://github.com/ben-manes/caffeine/wiki/Guava-zh-CN
- 通过异步维护/异步通知/异步刷新等方式, 达到了极致的读写性能
- 实现了JSR-107 JCache接口API
EhCache是一个轻量级开源的缓存框架, Hibernate使用的默认缓存框架就是EhCache, 它支持多种缓存模型, 将缓存管理在 堆内 / 堆外 / 磁盘 / 远程 多地.
特性:
- 实现了JSR107的规范, 并支持无缝集成 SpringCache/Hibernate/Tomcat等
- 轻量级核心, 除slf4j外无其他依赖
- 支持 堆内内存 / 堆外内存 / 磁盘 / 远程缓存服务 三层存储
- 支持多种缓存模型: 独立缓存 / 分布式缓存 / 复制式缓存 , 具体描述如下:
- 独立缓存: 每个应用实例在本地维护缓存, 意味着在其中一个实例中修改了缓存, 会导致与其他实例的缓存信息不一致
- 分布式缓存: 每个应用实例本地维护少量热点缓存, 并有一个远程缓存服务端来管理更多的缓存信息, 本地缓存未命中时则请求远程服务获取缓存信息, 这解决了缓存空间的问题, 但也无法保证实例间的本地缓存一致性
JetCache 是阿里开源的通用缓存访问框架, 它统一了多级缓存的访问方式, 封装了类似于SpringCache的注解, 以及GuavaCache类似的Builder, 来简化项目中使用缓存的难度
特性:
- 提供统一的, 类似jsr-107风格的API访问Cache, 并可通过注解创建并配置Cache实例
- 提供类似SpringCache风格的注解, 实现声明式的方法缓存, 并支持TTL和两级缓存
- 支持缓存自动刷新与家在保护, 防止高并发下缓存未命中时打爆数据库
- 支持缓存的统计指标
缓存一致性
上面介绍了几种本地缓存方案,选哪种你完全可以根据自己的熟练度来,如果都不熟悉就是用默认spring cache即可。
但其实在工作中,很多人偷懒,本地缓存也不加,只添加redis来做一级远程缓存,如果redis中没有,那么再从数据库中查询。
因为使用了缓存技术后,通常都涉及一个问题:缓存一致性
.
这个问题很头疼,涉及到你对系统架构的理解,以及根据架构去选择合适的一致性方案。所以通常能少上一点缓存手段,对于开发就能少一点事儿.
如何解决这个问题,推介阅读文末文章。
通过注解方式使用
介绍一下 Encache 的注解使用方式
@Cacheable
:启用缓存,首先从缓存中查找数据,如果存在,则从缓存读取数据;如果不存在,则执行方法,并将方法返回值添加到缓存@CachePut
:更新缓存,如果 condition 计算结果为 true,则将方法返回值添加到缓存中@CacheEvict
:删除缓存,根据 value 与 key 字段计算缓存地址,将缓存数据删除
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
@Cacheable(value = "user", key = "#userId")
public User findById(Integer userId) {
return userDAO.findById(userId);
}
@Override
@CachePut(value = "user", key = "#user.id", condition = "#user.id != null")
public User save(User user) {
user.setUpdateTime(new Date());
userDAO.save(user);
return userDAO.findById(user.getId());
}
@Override
@CacheEvict(value = "user", key = "#userId")
public boolean deleteById(Integer userId) {
return userDAO.deleteById(userId);
}
@Override
public List<User> findAll() {
return userDAO.findAll();
}
}
其他阅读
------ 如果文章对你有用,感谢右上角 >>>点赞 | 收藏 <<<