【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
<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
# 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
缓存配置类
@Configuration
public class CaffeineCacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
}
User实体
@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
public interface UserMapper extends BaseMapper<User> {
}
UserService
public interface UserService extends IService<User> {
void saveUser(User user);
User getUserById(Long id);
User updateUser(User user);
String deleteUserById(Long id);
}
@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
@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);
}
}
@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
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
缓存配置类
@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);
}
@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本地缓存