今天在项目中用到了SpringCache,也首次面试会问的各种缓存问题,话不多说进行正题。
几种基本注解的使用
1. @Cacheable
@Cacheable
是 Spring Cache 中最常用的注解,用于将方法的返回结果缓存起来,以后再调用该方法时,如果缓存中已有结果,就不执行方法,而是直接返回缓存中的结果。
用法:
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
// 查询数据库
return userRepository.findById(userId).orElse(null);
}
- value:指定缓存的名称,比如
"users"
。 - key:指定缓存的 key,默认使用方法参数。如果不指定,Spring Cache 会自动使用方法参数作为 key。
常见属性:
condition
:可以设置条件,只有满足条件时才缓存。unless
:可以设置条件,满足条件时不缓存。sync
:设置为true
,可以防止缓存击穿,确保同一时刻只有一个线程能查询数据库并将结果缓存。
示例:
@Cacheable(value = "users", key = "#userId", condition = "#userId > 0", unless = "#result == null")
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
2. @CachePut
@CachePut
表示不管缓存中是否有值,都会执行方法并将结果放入缓存。常用于更新缓存。
用法:
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// 更新数据库
return userRepository.save(user);
}
- value:缓存的名称。
- key:指定缓存的 key。
@CachePut
每次都会执行方法,并将返回值放入缓存中,用于保证缓存与数据库数据的一致性。
3. @CacheEvict
@CacheEvict
用于从缓存中移除数据,常用于删除或更新数据时同步清除缓存。
用法:
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) {
// 删除数据库中的用户
userRepository.deleteById(userId);
}
- value:缓存的名称。
- key:要移除的缓存的 key。
常见属性:
allEntries = true
:删除该缓存名称下的所有缓存。beforeInvocation = true
:在方法执行前清除缓存,默认是false
,即方法执行后再清除缓存。
示例:
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsersCache() {
// 清除所有缓存
}
4. @Caching
@Caching
可以同时组合使用多个缓存相关的注解,适用于复杂的缓存操作。
用法:
@Caching(
put = { @CachePut(value = "users", key = "#user.id") },
evict = { @CacheEvict(value = "cacheName", key = "#user.id") }
)
public User saveOrUpdateUser(User user) {
return userRepository.save(user);
}
通过 @Caching
,你可以同时定义多个 @Cacheable
、@CachePut
、@CacheEvict
注解,完成复杂的缓存逻辑。
5. @CacheConfig
@CacheConfig
用于类级别的注解,统一配置缓存的名称和其它属性,减少重复配置。
用法:
@CacheConfig(cacheNames = "users")
public class UserService {
@Cacheable(key = "#userId")
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
@CachePut(key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
}
cacheNames
:指定默认的缓存名称,类中所有的缓存注解会默认使用这个名称。
小结:
@Cacheable
:将方法的返回结果缓存。@CachePut
:更新缓存,即使缓存中有数据,也会重新执行方法并更新缓存。@CacheEvict
:从缓存中移除数据。@Caching
:组合多个缓存注解。@CacheConfig
:类级别统一配置缓存名称等属性。
配置
要使用SpringCache需要首先开启配置:
1. 引入依赖
首先,在你的 pom.xml
中添加 Spring Cache 相关的依赖,如果你使用的是基于内存的缓存(如 ConcurrentMapCache
),可以使用如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
如果你打算使用 Redis、EhCache 等其他缓存实现,还需要引入对应的依赖,例如 Redis 缓存:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 开启缓存支持
在你的主类Application或任意配置类上加上 @EnableCaching
注解:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
// 你可以在这里配置缓存相关的 Bean 或者使用默认配置
}
@SpringBootApplication
@EnableScheduling
@EnableCaching
public class MyAppApplication {
//也可以在主类上面加
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
3. 配置缓存(可选)
Spring Boot 默认使用 ConcurrentMapCache
,即内存中的缓存。如果需要使用其他缓存实现,你可以在 application.properties
中进行配置。比如使用 Redis 缓存:
# Redis
data:
redis:
port: 6379
host: ${your_host}
password: ${pwd_if_needed}
# Cache
cache:
type: redis # 使用redis作为cache
4. 使用缓存注解
一旦启用缓存支持后,你就可以使用 @Cacheable
, @CachePut
, @CacheEvict
等注解进行缓存操作。
小结:
- 引入依赖:添加
spring-boot-starter-cache
依赖。 - 开启缓存:在配置类上加
@EnableCaching
。 - 配置缓存类型:在
application.properties
中配置缓存类型(如 Redis 或默认内存缓存)。 - 使用注解:在服务方法上使用
@Cacheable
,@CachePut
,@CacheEvict
等缓存注解。
这就完成了 Spring Cache 的基本配置和开启。
现在再来说说在最近实际项目中的应用
JwtFilter:
在用户登陆的时候根据Token里解析出来的id去查询数据库,将查询的结果进行缓存,最终返回一个LoginUser对象给SringSecurity认证。
/**
* parse the token and get the user information to create a LoginUser object
* @param token token to parse
* @return LoginUser object
* @throws UsernameNotFoundException if the user is not found
* @Description: get the newest user information from the database if the user is not in the cache!
*/
private LoginUser getLoginUser(String token) {
// Parse the token to get the user information
Map<String, Object> claims = jwtUtil.parseToken(token);
// get the user information from the database can make sure the user is up to date
Long userId = Long.parseLong(claims.get("userId").toString());
// if the user is not in the cache, get the user from the database
User user = cachedUserService.getUserById(userId);
if (user != null) {
user.setPassword(null);// 不将密码传递保存在redis中
}else {
throw new UsernameNotFoundException("User not found");
}
List<String> roles = cachedUserService.getRolesByUserId(userId);
List<String> permissions = cachedUserService.getPermissionsByUserId(userId);
return new LoginUser(user, permissions, roles);
}
这里cachedUserService就是专门用来在经过jwtfilter的是手查询用户的最新的信息的,减少数据库的负担。
/**
* @Author: Yupeng Li
* @Date: 22/9/2024 02:58
* @Description: This class is used to cache user, role and permission data in Redis cache.
* when a new API request comes in, it will go through the JWT filter,
* and then the CachedUserService will be called to get the user, role and permission data from the cache.
* <p>
* If the data is not in the cache, it will be fetched from the database and then stored in the cache.
* If the data is already in the cache, it will be fetched from the cache directly.
* <p>
* when the user, role and permission data is updated, the cache will be evicted.
* <p>
* The aim of this class is to reduce the number of database queries and improve the performance of the application.
*/
@Service
public class CachedUserService {
@Resource
private UserMapper userMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private PermissionMapper permissionMapper;
private static final Logger logger = LoggerFactory.getLogger(CachedUserService.class);
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
return userMapper.finUserById(userId);
}
@Cacheable(value = "roles", key = "#userId")
public List<String> getRolesByUserId(Long userId) {
return roleMapper.findRolesByUserId(userId);
}
@Cacheable(value = "permissions", key = "#userId")
public List<String> getPermissionsByUserId(Long userId) {
return permissionMapper.findPermissionsByUserId(userId);
}
@CacheEvict(value = "users", key = "#userId")
public void evictUserCache(Long userId) {
logger.info("User cache evicted: {}", userId);
}
@CacheEvict(value = "roles", key = "#userId")
public void evictRolesCache(Long userId) {
logger.info("Roles cache evicted: {}", userId);
}
@CacheEvict(value = "permissions", key = "#userId")
public void evictPermissionsCache(Long userId) {
logger.info("Permissions cache evicted: {}", userId);
}
}
这里有一个坑要提别的提醒大家,就是在这个CachedUserService里面你不能直接在这个类里面去调用其他方法。比如你这个类里面有个evictAllCahe()
这个方法,你想在这个方法里面直接调用其他的三个方法evictRolesCache()
,evictUserCache()
,evictPermissionsCache()
很负责任的告诉你,当你在其他类里面调用evictAllCahe()
企图删除所有缓存时是不会生效的。方法可以执行,但是缓存不会被删除,别问我怎么知道的(被坑惨了哈哈哈。
主要原因好像是Spring的AOP机制什么什么的,没了解太深。反转正确的做法就是你吧evictAllCahe()
这个方法写到其他类里面去然后再调用就可以了,同一个类里面就不行!
public class LoginServiceImpl {
protected ResponseEntity<Result> completeProfileUpdate(User user) {
......
// 每当用户更新profile,移除所有的用户缓存,以便下次查询时重新加载数据!
evictAllUserCaches(user.getId());
......
return ResponseEntity.ok(Result.success("Profile updated successfully"));
}
/**
* 移除所有用户缓存
* @param userId 用户ID
*/
private void evictAllUserCaches(Long userId) {
cachedUserService.evictUserCache(userId);
cachedUserService.evictRolesCache(userId);
cachedUserService.evictPermissionsCache(userId);
}
}