Java系列 - 缓存方案

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
  1. 根据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;
    }
}
  1. Guava Cache

基于Guava Cache实现本地缓存。Guava是Google团队开源的一款 Java 核心增强库

特性:

  • 支持最大容量限制
  • 支持两种过期删除策略(插入时间和访问时间)
  • 支持简单的统计功能
  • 基于LRU算法实现
<dependency>
    <groupId>com.google.guavagroupId>
    <artifactId>guavaartifactId>
    <version>30.1.1-jreversion>
dependency>
  1. Caffeine

Caffeine是以 GuavaCache 为原型而开发的一个本地缓存框架, 相对GuavaCache, 它有更高的性能与命中率, 更强大的功能, 更灵活的配置方式

特性:

  • 具备GuavaCache几乎所有特性, 并提供了适配器, 方便Guava用户迁移至Caffeine, 两者差异见: https://github.com/ben-manes/caffeine/wiki/Guava-zh-CN
  • 通过异步维护/异步通知/异步刷新等方式, 达到了极致的读写性能
  • 实现了JSR-107 JCache接口API
  1. EhCache

EhCache是一个轻量级开源的缓存框架, Hibernate使用的默认缓存框架就是EhCache, 它支持多种缓存模型, 将缓存管理在 堆内 / 堆外 / 磁盘 / 远程 多地.

特性:

  • 实现了JSR107的规范, 并支持无缝集成 SpringCache/Hibernate/Tomcat等
  • 轻量级核心, 除slf4j外无其他依赖
  • 支持 堆内内存 / 堆外内存 / 磁盘 / 远程缓存服务 三层存储
  • 支持多种缓存模型: 独立缓存 / 分布式缓存 / 复制式缓存 , 具体描述如下:
  • 独立缓存: 每个应用实例在本地维护缓存, 意味着在其中一个实例中修改了缓存, 会导致与其他实例的缓存信息不一致
  • 分布式缓存: 每个应用实例本地维护少量热点缓存, 并有一个远程缓存服务端来管理更多的缓存信息, 本地缓存未命中时则请求远程服务获取缓存信息, 这解决了缓存空间的问题, 但也无法保证实例间的本地缓存一致性
  1. JetCache

JetCache 是阿里开源的通用缓存访问框架, 它统一了多级缓存的访问方式, 封装了类似于SpringCache的注解, 以及GuavaCache类似的Builder, 来简化项目中使用缓存的难度

特性:

  • 提供统一的, 类似jsr-107风格的API访问Cache, 并可通过注解创建并配置Cache实例
  • 提供类似SpringCache风格的注解, 实现声明式的方法缓存, 并支持TTL和两级缓存
  • 支持缓存自动刷新与家在保护, 防止高并发下缓存未命中时打爆数据库
  • 支持缓存的统计指标

缓存一致性

上面介绍了几种本地缓存方案,选哪种你完全可以根据自己的熟练度来,如果都不熟悉就是用默认spring cache即可。

但其实在工作中,很多人偷懒,本地缓存也不加,只添加redis来做一级远程缓存,如果redis中没有,那么再从数据库中查询。

因为使用了缓存技术后,通常都涉及一个问题:缓存一致性.

Java系列 - 缓存方案_第1张图片

这个问题很头疼,涉及到你对系统架构的理解,以及根据架构去选择合适的一致性方案。所以通常能少上一点缓存手段,对于开发就能少一点事儿.

如何解决这个问题,推介阅读文末文章。

通过注解方式使用

介绍一下 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();
    }
}

其他阅读

  • 1.主流缓存框架对比
  • 2.Springboot集成各种缓存框架
  • 3.缓存最终一致性的解决方案

------ 如果文章对你有用,感谢右上角 >>>点赞 | 收藏 <<<

你可能感兴趣的:(java,缓存,jvm)