【SpringBoot应用篇】SpringBoot集成Caffeine本地缓存

【SpringBoot应用篇】SpringBoot集成Caffeine本地缓存

本地缓存介绍

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。

之前介绍过 Redis 这种 NoSql 作为缓存组件,它能够很好的作为分布式缓存组件提供多个服务间的缓存,但是 Redis 这种还是需要网络开销,增加时耗。

本地缓存是直接从本地内存中读取,没有网络开销,例如秒杀系统或者数据量小的缓存等,比远程缓存更合适。

本地缓存方案选型

1、 基于ConcurrentHashMap实现本地缓存

缓存的本质就是存储在内存中的KV数据结构,对应的就是jdk中线程安全的ConcurrentHashMap,但是要实现缓存,还需要考虑淘汰、最大限制、缓存过期时间淘汰等等功能;

优点是实现简单,不需要引入第三方包,比较适合一些简单的业务场景。缺点是如果需要更多的特性,需要定制化开发,成本会比较高,并且稳定性和可靠性也难以保障。对于比较复杂的场景,建议使用比较稳定的开源工具。

2、基于Guava Cache实现本地缓存

Guava是Google团队开源的一款 Java 核心增强库,包含集合、并发原语、缓存、IO、反射等工具箱,性能和稳定性上都有保障,应用十分广泛。Guava Cache支持很多特性:

  • 支持最大容量限制
  • 支持两种过期删除策略(插入时间和访问时间)
  • 支持简单的统计功能
  • 基于LRU算法实现

3、基于Caffeine实现本地缓存

Caffeine是基于java8实现的新一代缓存工具,缓存性能接近理论最优。可以看作是Guava Cache的增强版,功能上两者类似,不同的是Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,在性能上有明显的优越性

4、 基于Ehcache实现本地缓存

Ehcache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。同Caffeine和Guava Cache相比,Ehcache的功能更加丰富,扩展性更强:

  • 支持多种缓存淘汰算法,包括LRU、LFU和FIFO
  • 缓存支持堆内存储、堆外存储、磁盘存储(支持持久化)三种
  • 支持多种集群方案,解决数据共享问题

Caffeine

在项目开发中,为提升系统性能,减少 IO 开销,本地缓存是必不可少的。最常见的本地缓存是 Guava 和 Caffeine。

Caffeine 是基于 Google Guava Cache 设计经验改进的结果,相较于 Guava 在性能和命中率上更具有效率,你可以认为其是 Guava Plus。

Caffeine 是基于Java 8 开发的、提供了近乎最佳命中率的高性能本地缓存组件,Spring5 开始不再支持 Guava Cache,改为使用 Caffeine。

在下面缓存组件中 Caffeine 性能是其中最好的

SpringBoot 集成 Caffeine 两种方式

SpringBoot 有俩种使用 Caffeine 作为缓存的方式:

方式一: 直接引入 Caffeine 依赖,然后使用 Caffeine 提供的api方法实现本地缓存。

方式二: 引入 Caffeine 和 Spring Cache 依赖,使用 SpringCache 注解方法实现本地缓存。

SpringBoot 集成 Caffeine 方式一

pom

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
     <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.14</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

application.yml

yml 复制代码
# DataSource Config
spring:
  datasource:
    #   数据源基本配置
    url: jdbc:mysql://localhost:3306/study_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialization-mode: always #表示始终都要执行初始化,2.x以上版本需要加上这行配置
    type: com.alibaba.druid.pool.DruidDataSource
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

# Logger Config
logging:
  level:
    cn.zysheep.mapper: debug

缓存配置类

java 复制代码
@Configuration
public class CaffeineCacheConfig {
    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                // 设置最后一次写入或访问后经过固定时间过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(1000)
                .build();
    }
}

User实体

java 复制代码
@TableName(value ="tb_user")
@Data
public class User implements Serializable {
    /**
     * 用户ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     */
    @TableField("username")
    private String userName;

    /**
     * 现在住址
     */
    @TableField("address")
    private String address;
}

UserMapper

java 复制代码
public interface UserMapper extends BaseMapper<User> {

}

UserService

java 复制代码
public interface UserService extends IService<User> {

    void saveUser(User user);
    
    User getUserById(Long id);

    User updateUser(User user);

    String deleteUserById(Long id);
}
java 复制代码
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);


    @Autowired
    private Cache<String, Object> caffeineCache;

    @Override
    public void saveUser(User user) {
        save(user);
        // 加入缓存
        caffeineCache.put(String.valueOf(user.getId()),user);
    }

    /**
     * 查询用户信息,并缓存结果
     *
     * @param id
     * @return
     */
    public User getUserById(Long id) {
        // 先从缓存读取
        caffeineCache.getIfPresent(id);
        User user = (User) caffeineCache.asMap().get(String.valueOf(id));
        if (Objects.nonNull(user)) {
            return user;

        }
        // 如果缓存中不存在,则从库中查找
        user = getById(id);
        // 如果用户信息不为空,则加入缓存
        if (user != null) {
            caffeineCache.put(String.valueOf(user.getId()), user);
        }

        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", user);
        return user;
    }


    /**
     * 更新用户信息
     * @param user
     * @return
     */
    public User updateUser(User user) {
        log.info("user: {}", user);
        updateById(user);
        User user1 = getById(user.getId());
        // 替换缓存中的值
        caffeineCache.put(String.valueOf(user1.getId()), user1);
        return user1;
    }

    public String deleteUserById(Long id) {
        boolean b = removeById(id);
        if (b) {
            // 从缓存中删除
            caffeineCache.asMap().remove(String.valueOf(id));
        }
        return b ? "删除成功" : "删除失败";
    }
}

UserController

java 复制代码
@Getter
@Setter
@SuppressWarnings({"AlibabaClassNamingShouldBeCamel"})
@Accessors(chain = true)
public class R<T> {
    public static final String DEF_ERROR_MESSAGE = "系统繁忙,请稍候再试";
    public static final String HYSTRIX_ERROR_MESSAGE = "请求超时,请稍候再试";
    public static final int SUCCESS_CODE = 0;
    public static final int FAIL_CODE = -1;
    public static final int TIMEOUT_CODE = -2;
    /**
     * 统一参数验证异常
     */
    public static final int VALID_EX_CODE = -9;
    public static final int OPERATION_EX_CODE = -10;
    /**
     * 调用是否成功标识,0:成功,-1:系统繁忙,此时请开发者稍候再试 详情见[ExceptionCode]
     */
    private int code;

    /**
     * 调用结果
     */
    private T data;

    /**
     * 结果消息,如果调用成功,消息通常为空T
     */
    private String msg = "ok";


    private String path;
    /**
     * 附加数据
     */
    private Map<String, Object> extra;

    /**
     * 响应时间
     */
    private long timestamp = System.currentTimeMillis();

    private R() {
        super();
    }

    public R(int code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public static <E> R<E> result(int code, E data, String msg) {
        return new R<>(code, data, msg);
    }

    /**
     * 请求成功消息
     *
     * @param data 结果
     * @return RPC调用结果
     */
    public static <E> R<E> success(E data) {
        return new R<>(SUCCESS_CODE, data, "ok");
    }

    public static R<Boolean> success() {
        return new R<>(SUCCESS_CODE, true, "ok");
    }

    /**
     * 请求成功方法 ,data返回值,msg提示信息
     *
     * @param data 结果
     * @param msg  消息
     * @return RPC调用结果
     */
    public static <E> R<E> success(E data, String msg) {
        return new R<>(SUCCESS_CODE, data, msg);
    }

    /**
     * 请求失败消息
     *
     * @param msg
     * @return
     */
    public static <E> R<E> fail(int code, String msg) {
        return new R<>(code, null, (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> R<E> fail(String msg) {
        return fail(OPERATION_EX_CODE, msg);
    }

    public static <E> R<E> fail(String msg, Object... args) {
        String message = (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg;
        return new R<>(OPERATION_EX_CODE, null, String.format(message, args));
    }

    public static <E> R<E> fail(BaseExceptionCode exceptionCode) {
        return validFail(exceptionCode);
    }

    public static <E> R<E> fail(BizException exception) {
        if (exception == null) {
            return fail(DEF_ERROR_MESSAGE);
        }
        return new R<>(exception.getCode(), null, exception.getMessage());
    }

    /**
     * 请求失败消息,根据异常类型,获取不同的提供消息
     *
     * @param throwable 异常
     * @return RPC调用结果
     */
    public static <E> R<E> fail(Throwable throwable) {
        return fail(FAIL_CODE, throwable != null ? throwable.getMessage() : DEF_ERROR_MESSAGE);
    }

    public static <E> R<E> validFail(String msg) {
        return new R<>(VALID_EX_CODE, null, (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> R<E> validFail(String msg, Object... args) {
        String message = (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg;
        return new R<>(VALID_EX_CODE, null, String.format(message, args));
    }

    public static <E> R<E> validFail(BaseExceptionCode exceptionCode) {
        return new R<>(exceptionCode.getCode(), null,
                (exceptionCode.getMsg() == null || exceptionCode.getMsg().isEmpty()) ? DEF_ERROR_MESSAGE : exceptionCode.getMsg());
    }

    public static <E> R<E> timeout() {
        return fail(TIMEOUT_CODE, HYSTRIX_ERROR_MESSAGE);
    }


    public R<T> put(String key, Object value) {
        if (this.extra == null) {
            this.extra = Maps.newHashMap();
        }
        this.extra.put(key, value);
        return this;
    }

    /**
     * 逻辑处理是否成功
     *
     * @return 是否成功
     */
    public Boolean getIsSuccess() {
        return this.code == SUCCESS_CODE || this.code == 200;
    }

    /**
     * 逻辑处理是否失败
     *
     * @return
     */
    public Boolean getIsError() {
        return !getIsSuccess();
    }

    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}
java 复制代码
@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;


    @PostMapping("save")
    public R saveUser(@RequestBody User user) {
        userService.saveUser(user);
        return R.success(null);
    }


    @GetMapping("getById")
    public R getById(@RequestParam Long id) {
        User user = userService.getUserById(id);
        return R.success(user);
    }

    @GetMapping("getByIdNoCache")
    public R getByNameNoCache(@RequestParam Long id) {
        List<User> users = userService.getUserByIdNoCache(id);
        return R.success(users);
    }


    @PostMapping("updateUser")
    public R updateUser(User user) {
        return R.success(userService.updateUser(user));
    }


    @PostMapping("deleteUserById")
    public R deleteUserById(Long id) {
        return R.success(userService.deleteUserById(id));
    }
}

SpringBoot 集成 Caffeine 方式二

引入 Caffeine 和 Spring Cache 依赖,使用 SpringCache 注解方法实现本地缓存。

pom

方式一的依赖中添加spring-boot-starter-cache依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

缓存配置类

java 复制代码
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        //Caffeine配置
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                //最后一次写入后经过固定时间过期
                .expireAfterWrite(60*5, TimeUnit.SECONDS)
                //maximumSize=[long]: 缓存的最大条数
                .maximumSize(1000);
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }

//    @Bean
//    public Cache<String, Object> caffeineCache() {
//        return Caffeine.newBuilder()
//                // 设置最后一次写入或访问后经过固定时间过期
//                .expireAfterWrite(60, TimeUnit.SECONDS)
//                // 初始的缓存空间大小
//                .initialCapacity(100)
//                // 缓存的最大条数
//                .maximumSize(1000)
//                .build();
//    }
}

UserService

复制代码
public interface UserService extends IService<User> {

    void saveUser(User user);

    List<User> getUserByIdNoCache(Long id);

    User getUserById(Long id);

    User updateUser(User user);

    List<User> getUserByIdAndName(Long id, String userName);

    List<User> getUser(User user);

    String deleteUserById(Long id);
}
java 复制代码
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);


//    @Autowired
//    private Cache<String, Object> caffeineCache;

    @Override
    public void saveUser(User user) {
        save(user);
        // 加入缓存
       // caffeineCache.put(String.valueOf(user.getId()),user);
    }

    public List<User> getUserByIdNoCache(Long id) {
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(Objects.nonNull(id), User::getId, id);
        List<User> users = list(queryWrapper);
        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", users);
        return users;
    }



    /**
     * 查询用户信息,并缓存结果
     *
     * @param id
     * @return
     */
    @Cacheable(cacheNames = "user", key = "#id")
    public User getUserById(Long id) {
        // 先从缓存读取
//        caffeineCache.getIfPresent(id);
//        User user = (User) caffeineCache.asMap().get(String.valueOf(id));
//        if (Objects.nonNull(user)) {
//            return user;
//
//        }
        // 如果缓存中不存在,则从库中查找
        User user = getById(id);
        // 如果用户信息不为空,则加入缓存
//        if (user != null) {
//            caffeineCache.put(String.valueOf(user.getId()), user);
//        }

        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", user);
        return user;
    }

    // spEL使用"T(Type)"来表示 java.lang.Class 实例,"Type"必须是类全限定名,"java.lang"包除外。
    @Cacheable(cacheNames = "user", key = "T(String).valueOf(#id).concat('::').concat(#userName)")
    public List<User> getUserByIdAndName(Long id, String userName) {
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery()
                .like(StringUtils.isNotBlank(userName), User::getUserName, userName)
                .eq(Objects.nonNull(id), User::getId, id);
        List<User> users = list(queryWrapper);
        log.info("从数据库中读取,而非从缓存读取!");
        return users;
    }

    @Cacheable(cacheNames = "user", key = "#user.userName")
    public List<User> getUser(User user) {
        LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(StringUtils.isNotBlank(user.getUserName()), User::getUserName, user.getUserName());
        List<User> users = list(queryWrapper);
        log.info("从数据库中读取,而非从缓存读取!");
        return users;
    }

    /**
     * 更新用户信息
     *
     * @param user
     * @return
     */
    @CachePut(cacheNames = "user", key = "#result.id")
    public User updateUser(User user) {
        log.info("user: {}", user);
        updateById(user);
        User user1 = getById(user.getId());


        // 替换缓存中的值
        // caffeineCache.put(String.valueOf(user1.getId()), user1);

        return user1;
    }

    @CacheEvict(cacheNames = "user", beforeInvocation = true, key = "#id")
    public String deleteUserById(Long id) {
        boolean b = removeById(id);
//        if (b) {
//            // 从缓存中删除
//            caffeineCache.asMap().remove(String.valueOf(id));
//        }
        //  int i = 1 / 0;
        return b ? "删除成功" : "删除失败";
    }
}

标注缓存注解

  • @Cacheable: @Cacheble注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。这个注解一般用在查询方法上。
  • @CacheEvict: @CacheEvict注解的方法,会清空指定缓存。一般用在更新或者删除的方法上。
  • @CachePut : @CachePut注解的方法,保证方法被调用,又希望结果被缓存。会把方法的返回值put到缓存里面缓存起来。它通常用 在新增方法上。
  • @Caching :定义复杂的缓存规则
  • @CacheConfig:抽取缓存的公共配置

注解的具体用法: 【SpringBoot高级篇】SpringBoot集成cache本地缓存

相关推荐
星星点点洲23 分钟前
【缓存与数据库结合最终方案】伪从技术
数据库·缓存
武昌库里写JAVA27 分钟前
39.剖析无处不在的数据结构
java·vue.js·spring boot·课程设计·宠物管理
画个大饼1 小时前
Go语言实战:快速搭建完整的用户认证系统
开发语言·后端·golang
Ivan陈哈哈6 小时前
Redis是单线程的,如何提高多核CPU的利用率?
数据库·redis·缓存
李白的粉6 小时前
基于springboot的在线教育系统
java·spring boot·毕业设计·课程设计·在线教育系统·源代码
小马爱打代码7 小时前
SpringBoot原生实现分布式MapReduce计算
spring boot·分布式·mapreduce
iuyou️7 小时前
Spring Boot知识点详解
java·spring boot·后端
一弓虽7 小时前
SpringBoot 学习
java·spring boot·后端·学习
姑苏洛言7 小时前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
头顶秃成一缕光7 小时前
Redis的主从模式和哨兵模式
数据库·redis·缓存