Spring 缓存优化:不用花大钱也能让接口飞起来

最近做项目时,被接口性能问题搞得头都大了。第三方压测结果显示,接口平均响应时间超过 500ms,高峰期甚至破了 1 秒,QPS 也远远达不到预期。领导说换数据库、升服务器成本太高,让我想想低成本的解决办法。琢磨来琢磨去,发现 Spring 缓存是个好东西,今天就把这段优化经历分享给大家。

为啥要用缓存?

其实道理很简单,接口慢大多是因为数据库查询太频繁,CPU 和内存都扛不住。如果把那些经常被访问的数据存在缓存里,就不用每次都去麻烦数据库了,这样响应速度自然就快了。我当时就想着,要是能把热门数据的响应时间降到 50ms 以内,QPS 再提升个几倍,那就完美了。

快速上手 Spring 缓存

说干就干,在 Spring Boot 项目里用缓存其实很简单。先加个依赖:

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

然后在启动类上加上 @EnableCaching 注解,缓存功能就启用了。

@SpringBootApplication

@EnableCaching

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

这几个缓存注解得好好说说,平时开发常用到:

  • @Cacheable:查数据的时候先看缓存里有没有,有就直接拿,没有就查完了存进去。
  • @CachePut:更新数据的时候用,每次都会执行方法,然后把结果更新到缓存里。
  • @CacheEvict:删除数据后,把缓存里对应的也删掉,省得数据不一致。
  • @Caching:可以组合上面这些操作,比如又更新又删除的时候用。

实战:给用户查询接口提速

之前做的用户查询接口,直接查数据库,慢得不行,响应时间动不动就 200-500ms。

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Override
public User getUserById(Long id) {

return userRepository.findById(id).orElse(null);

}

后来加上缓存,效果立竿见影。查询的时候用 @Cacheable,更新用 @CachePut,删除用 @CacheEvict,一一对应上。

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Cacheable(value = "users", key = "#id", unless = "#result == null")
@Override
public User getUserById(Long id) {

return userRepository.findById(id).orElse(null);

}

@CachePut(value = "users", key = "#user.id")
@Override
public User updateUser(User user) {

return userRepository.save(user);

}

@CacheEvict(value = "users", key = "#id")
@Override
public void deleteUser(Long id) {

userRepository.deleteById(id);

}

缓存配置也简单,单节点应用用 ConcurrentHashMap 就行。

@Configuration
public class CacheConfig {

@Bean
public CacheManager cacheManager() {

return new ConcurrentMapCacheManager("users");

}

新问题:缓存导致权限判断失效

本以为加了缓存万事大吉,结果测试同学反馈了个棘手问题:用户权限修改后,接口的权限判断经常失灵。比如管理员明明已经把某个用户的 "删除权限" 收回了,但该用户还是能调用删除接口,直到过了很久或者重启服务才恢复正常。

排查了半天才发现症结所在:我们的权限判断逻辑依赖于 User 对象里的 role 字段,而这个 User 对象被缓存起来了。当管理员在后台修改用户角色时,虽然数据库里的 role 字段更新了,但缓存里的 User 对象还是旧的,导致权限校验时始终读取到过期的角色信息。

解决办法

针对这个问题,我们调整了缓存策略:

  1. 拆分缓存数据:把用户基本信息和权限信息分开存储,权限信息单独缓存并设置较短的过期时间(5 分钟)
// 基本信息缓存时间较长

@Cacheable(value = "userBaseInfo", key = "#id")

public UserBaseInfo getUserBaseInfo(Long id) { 
...
 }

// 权限信息缓存时间短

@Cacheable(value = "userPermissions", key = "#id", ttl = 300)

public List<String> getUserPermissions(Long id) { 
...
 }
  1. 权限变更时主动刷新:在角色修改接口中,主动删除对应用户的权限缓存
@CacheEvict(value = "userPermissions", key = "#userId")

public void updateUserRole(Long userId, List<String> newRoles) {

// 更新数据库角色信息

userRoleRepository.updateRoles(userId, newRoles);

}
  1. 关键接口增加实时校验:对于删除、修改等高危操作,在缓存校验后再做一次数据库实时校验
public void deleteResource(Long resourceId, Long userId) {

// 先查缓存做初步校验

List<String> permissions = getUserPermissions(userId);

if (!permissions.contains("DELETE_RESOURCE")) {

throw new AccessDeniedException("无权限");

}

// 关键操作再查一次数据库确认

if (!userPermissionRepository.hasPermission(userId, "DELETE_RESOURCE")) {

throw new AccessDeniedException("无权限");

}

// 执行删除操作

...

}

调整后,权限变更的生效延迟控制在了 5 分钟内,配合主动刷新机制,基本解决了权限判断失效的问题。

进阶玩法:Redis 缓存

要是分布式应用,内存缓存就不够用了,Redis 是个好选择。先加依赖:

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

然后配置一下,设置个过期时间,选个合适的序列化方式。

@Configuration
public class RedisCacheConfig {

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {

    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(30))
        .serializeKeysWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new StringRedisSerializer()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(factory)
        .cacheDefaults(config)
        .build();

}

缓存参数还能玩出花样,比如只给 ID 大于 0 的缓存,结果为空就不缓存,还能开同步防止缓存击穿。

@Cacheable(value = "users",key = "#id",condition = "#id > 0",unless = "#result == null || #result.age < 18",sync = true)
public User getUser(Long id) { ... }

那些踩过的坑和解决办法

用缓存久了,难免遇到些问题。比如总有人查不存在的数据,数据库压力大,这就是缓存穿透,缓存个空值或者用布隆过滤器能解决。

热点 key 过期的时候,一堆请求冲去查数据库,缓存击穿了,加个 sync=true 或者分布式锁就行。

大量缓存同时过期,数据库直接扛不住,缓存雪崩了,设置随机过期时间或者搞个多级缓存能缓解。

还有缓存和数据对不上的问题,先更数据库再删缓存,或者用消息队列异步更新都靠谱。

监控也很重要

缓存命中率得盯着,理想情况得超过 90%,还有内存使用量、过期淘汰率这些指标。Redis Insight 可视化做得不错,Spring Boot Actuator 能暴露缓存指标,想搞复杂点就用 Prometheus 加 Grafana 整个监控大盘。

总结一下

不用换数据库、不用升服务器,加个 Spring 缓存就能让接口快不少。但缓存不是银弹,像权限这类动态变化的数据就需要特别处理。先从简单的内存缓存试试水,再过渡到 Redis,注意避开那些常见的坑,再整个监控体系,接口响应时间降个 80%-90%,QPS 提个 3-5 倍不是问题,数据库压力也能大减。亲测有效的低成本方案,赶紧试试吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

练习时长两年半的程序员小胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值