使用 Spring Cache 实现缓存,这种方式才叫优雅!

别再用 HashMap 当缓存,也别再手动写 RedisTemplate 操作缓存了,赶紧把 Spring Cache 用起来。刚开始可能会觉得配置有点麻烦,但一旦上手,你会发现, 这玩意儿是真的香!​

兄弟们,咱们做 Java 开发的,谁没踩过性能的坑啊?前阵子我朋友小杨就跟我吐槽,说他写的用户查询接口,本地测试的时候唰唰快,一上生产环境,用户量稍微一上来,数据库直接被干到 CPU 100%,日志里全是红色的慢查询警告。老板在旁边盯着,他手忙脚乱地改代码,最后没办法,手动写了个 HashMap 当缓存应急 。 结果嘛,你懂的,多实例部署的时候缓存不一致,又加班到半夜。

其实这种事儿真不用这么狼狈,Spring 早就给咱们准备好了解决方案 ——Spring Cache。这玩意儿就像个 “缓存管家”,你不用手动写代码操作 Redis、Ehcache 这些中间件,只要给方法贴个注解,它就帮你把 “查缓存→有就返回→没有查库→存缓存” 这一套流程全搞定。今天咱们就好好聊聊,怎么用 Spring Cache 实现优雅的缓存,让你从此告别 “手动缓存火葬场”。

一、先搞明白:Spring Cache 到底是个啥?

可能有人会说,“缓存不就是存数据嘛,我自己写个工具类调用 Redis 也能搞定”。这话没毛病,但你想想,要是每个查询方法都写一遍 “查缓存、存缓存” 的逻辑,代码得有多冗余?而且万一以后要换缓存中间件,从 Redis 换成 Memcached,不得每个方法都改一遍?

Spring Cache 的核心思路就是 “解耦”—— 把缓存逻辑和业务逻辑分开。它基于 AOP(面向切面编程)实现,当你调用一个加了缓存注解的方法时,Spring 会先拦截这个调用,帮你处理缓存相关的操作,业务代码里完全不用管缓存的事儿。

打个比方,你就像餐厅里的厨师(负责业务逻辑),Spring Cache 就是服务员(负责缓存)。客人要一份宫保鸡丁(调用方法),服务员会先去备餐区看看有没有做好的(查缓存),有就直接端给客人;没有就告诉厨师做一份(执行业务逻辑),做好后再把一份放进备餐区(存缓存),下次客人再要就不用麻烦厨师了。你看,厨师全程不用管备餐区的事儿,专心做菜就行 —— 这就是优雅!

而且 Spring Cache 是 “抽象层”,它不关心底层用的是 Redis 还是 Ehcache,你只要配置好对应的 “缓存管理器”,就能无缝切换。比如开发环境用内存缓存(ConcurrentMapCache)方便测试,生产环境换成 Redis,业务代码一行都不用改 —— 这波操作谁看了不夸一句?

二、快速上手:3 步搞定 Spring Cache 基础使用

光说不练假把式,咱们先从最基础的例子开始,用 Spring Boot+Spring Cache+Redis 实现一个用户查询的缓存功能。别担心,步骤很简单,跟着做就行。

第一步:搭环境(引入依赖)

首先得有个 Spring Boot 项目,然后在 pom.xml 里加两个关键依赖:Spring Cache 的起步依赖,还有 Redis 的起步依赖(毕竟生产环境大多用 Redis)。

<!-- Spring Cache 起步依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 起步依赖(底层用 lettuce 客户端) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 要是用Jackson序列化,再加个这个(后面会讲为啥需要) -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

如果是 Gradle 项目,对应的依赖也差不多,这里就不赘述了。记住,Spring Boot 2.x 和 3.x 的依赖坐标基本一致,不用纠结版本问题(除非你用的是特别老的 2.0 之前的版本,那得升级了兄弟)。

第二步:开开关(加注解启用缓存)

在 Spring Boot 的启动类上,加个@EnableCaching注解,告诉 Spring “我要启用缓存功能啦”。就像你开空调之前要按一下电源键一样简单。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 关键:启用Spring Cache
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

第三步:写业务(给方法贴缓存注解)

接下来就是核心了 —— 给业务方法加缓存注解。咱们先定义一个 User 实体类,再写个 UserService,里面有个根据 id 查询用户的方法,给这个方法加@Cacheable注解。

先看 User 实体类(注意要实现 Serializable,后面讲序列化会用到):

import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data // lombok注解,省掉getter、setter
public class User implements Serializable { // 实现Serializable,Redis序列化需要
    private Long id;
    private String username;
    private String phone;
    private LocalDateTime createTime;
}

然后是 UserService,这里咱们模拟数据库查询(用 Thread.sleep 模拟慢查询,突出缓存的作用):

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
    // 模拟数据库查询:根据id查用户
    // @Cacheable:表示这个方法的结果要被缓存
    // value:缓存的“命名空间”,相当于给缓存分个组,避免key冲突
    // key:缓存的key,这里用SpEL表达式,取方法参数id的值
    @Cacheable(value = "userCache", key = "#id")
    public User getUserById(Long id) {
        // 模拟数据库查询的耗时操作(比如查MySQL)
        try {
            TimeUnit.SECONDS.sleep(2); // 睡2秒,模拟慢查询
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 模拟从数据库查出来的数据
        User user = new User();
        user.setId(id);
        user.setUsername("用户" + id);
        user.setPhone("1380013800" + (id % 10));
        user.setCreateTime(LocalDateTime.now());
        return user;
    }
}

最后写个 Controller 测试一下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        long start = System.currentTimeMillis();
        User user = userService.getUserById(id);
        long end = System.currentTimeMillis();
        System.out.println("查询耗时:" + (end - start) + "毫秒");
        return user;
    }
}

现在启动项目,用 Postman 或者浏览器访问http://localhost:8080/user/1:

第一次访问:控制台会打印 “查询耗时:约 2000 毫秒”,因为要走数据库查询,然后把结果存到 Redis。

第二次访问:控制台直接打印 “查询耗时:约 10 毫秒”,因为直接从 Redis 拿缓存了!

你看,就加了个@Cacheable注解,缓存就生效了,业务代码里完全没写任何 Redis 相关的逻辑 —— 这难道不优雅吗?比你手动写redisTemplate.opsForValue().get()、redisTemplate.opsForValue().set()清爽多了吧?

三、核心注解:这 5 个注解搞定 90% 的缓存场景

刚才用了@Cacheable,但 Spring Cache 的本事可不止这一个。它总共提供了 5 个核心注解,覆盖了 “查、增、改、删” 所有缓存操作。咱们一个个讲,结合实际业务场景,保证你一看就懂。

1. @Cacheable:查缓存,有则用,无则存

这是最常用的注解,作用是 “查询缓存”:调用方法前先查缓存,如果缓存存在,直接返回缓存的值,不执行方法;如果缓存不存在,执行方法,把结果存到缓存里。

刚才的例子已经用过了,这里再补充几个关键属性,都是实战中必用的:

属性

作用

例子

value

缓存命名空间(必填),可以理解为缓存的 “文件夹”,避免 key 冲突

value = "userCache"

key

缓存的 key(可选),用 SpEL 表达式,默认是所有参数的组合

key = "#id"(取参数 id)、key = "#user.id"(取对象参数的 id)

condition

缓存的 “前置条件”(可选),满足条件才缓存,SpEL 表达式返回 boolean

condition = "#id > 100"(只有 id>100 才缓存)

unless

缓存的 “排除条件”(可选),方法执行后判断,满足则不缓存

unless = "#result == null"(结果为 null 不缓存)

cacheManager

指定用哪个缓存管理器(可选),比如有的方法用 Redis,有的用 Ehcache

cacheManager = "redisCacheManager"

举个带条件的例子,比如 “只缓存 id 大于 100 的用户,并且结果不为 null”:

@Cacheable(
    value = "userCache",
    key = "#id",
    condition = "#id > 100", // 前置条件:id>100才查缓存/存缓存
    unless = "#result == null" // 排除条件:结果为null不存缓存
)
public User getUserById(Long id) {
    // 业务逻辑不变...
}

这里要注意condition和unless的区别:condition是在方法执行前判断的,如果不满足,连缓存都不查,直接执行方法;unless是在方法执行后判断的,不管怎么样都会执行方法,只是结果不存缓存。别搞混了哈!

2. @CachePut:更新缓存,先执行方法再存缓存

比如你更新用户信息的时候,得把缓存里的旧数据更成新的吧?这时候就用@CachePut。它的逻辑是:先执行方法(不管缓存有没有),然后把方法的结果存到缓存里(覆盖旧的缓存,如果有的话)。

举个更新用户的例子:

// @CachePut:更新缓存,执行方法后把结果存到缓存
// key和查询方法保持一致,都是#user.id,这样才能覆盖旧缓存
@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
    // 模拟更新数据库(这里省略JDBC/MyBatis代码)
    try {
        TimeUnit.SECONDS.sleep(1); // 模拟耗时
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 模拟更新后的用户数据(实际应该从数据库查最新的)
    user.setCreateTime(LocalDateTime.now()); // 更新时间
    return user;
}

比如你先查了 user.id=1(缓存里存了旧数据),然后调用 updateUser 更新 user.id=1 的信息,@CachePut会先执行更新逻辑,再把新的 user 对象存到缓存里,覆盖原来的旧缓存。下次再查 user.id=1,拿到的就是最新的数据了 —— 完美解决缓存和数据库不一致的问题。

3. @CacheEvict:删除缓存,执行方法后清缓存

当你删除用户的时候,缓存里的旧数据也得删掉吧?不然别人还能查到已经删除的用户,这就出 bug 了。@CacheEvict就是干这个的,它的逻辑是:执行方法(比如删除数据库记录),然后删除对应的缓存。

举个删除用户的例子:

// @CacheEvict:删除缓存,执行方法后删除指定缓存
@CacheEvict(value = "userCache", key = "#id")
public void deleteUser(Long id) {
    // 模拟删除数据库记录
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("用户" + id + "已从数据库删除");
}

还有个常用的属性allEntries,比如你更新了用户列表,想把 “userCache” 这个命名空间下的所有缓存都删掉(避免列表缓存和数据库不一致),就可以设allEntries = true:

// allEntries = true:删除value="userCache"下的所有缓存
@CacheEvict(value = "userCache", allEntries = true)
public void batchUpdateUser(List<User> userList) {
    // 批量更新数据库逻辑...
}

另外还有个beforeInvocation属性,默认是false(方法执行后删缓存),如果设为true,会在方法执行前删缓存。什么时候用呢?比如方法执行可能会抛异常,你又希望不管成功失败都删缓存,就可以用这个。不过一般情况下用默认的false就行,避免方法执行失败了,缓存却被删了,导致下次查询穿透到数据库。

4. @Caching:组合注解,一次搞定多个缓存操作

有时候一个方法需要同时做多个缓存操作,比如 “更新用户信息后,既要更新用户的缓存,又要删除用户列表的缓存”,这时候单个注解就不够用了,得用@Caching来组合。

@Caching里可以包含cacheable、put、evict三个属性,每个属性都是一个数组,可以放多个对应的注解。

举个实战例子:更新用户信息后,更新用户缓存(@CachePut),同时删除用户列表的缓存(@CacheEvict):

// @Caching:组合多个缓存操作
@Caching(
    put = {
        // 更新用户缓存(key是用户id)
        @CachePut(value = "userCache", key = "#user.id")
    },
    evict = {
        // 删除用户列表缓存(假设列表缓存的key是"userList")
        @CacheEvict(value = "userListCache", key = "'userList'")
    }
)
public User updateUserAndClearListCache(User user) {
    // 更新数据库逻辑...
    user.setCreateTime(LocalDateTime.now());
    returnuser;
}

这样一来,调用这个方法的时候,Spring 会同时执行@CachePut和@CacheEvict两个操作,既更新了单个用户的缓存,又清空了列表缓存 —— 不用写两个方法,也不用手动操作缓存,太方便了!

5. @CacheConfig:类级注解,统一配置缓存属性

如果一个 Service 里的所有方法都用同一个value(缓存命名空间)或者cacheManager,那每个方法都写一遍岂不是很麻烦?@CacheConfig就是用来解决这个问题的,它是类级别的注解,可以统一配置当前类所有缓存方法的公共属性。

比如 UserService 里的所有方法都用value = "userCache",就可以这么写:

import org.springframework.cache.annotation.CacheConfig;
importorg.springframework.cache.annotation.Cacheable;
importorg.springframework.stereotype.Service;

@Service
// @CacheConfig:统一配置当前类的缓存属性
@CacheConfig(value = "userCache") 
publicclassUserService {

    // 不用再写value="userCache"了,继承类上的配置
    @Cacheable(key = "#id")
    public User getUserById(Long id) {
        // 逻辑...
    }

    // 同样不用写value,key还是要写(因为每个方法的key可能不一样)
    @CachePut(key = "#user.id")
    public User updateUser(User user) {
        // 逻辑...
    }

    // 也不用写value
    @CacheEvict(key = "#id")
    public void deleteUser(Long id) {
        // 逻辑...
    }
}

注意哈,@CacheConfig只能配置公共属性,像key这种每个方法可能不一样的属性,还是得在方法上单独写。而且方法上的配置会覆盖类上的配置,比如你在方法上写了value = "otherCache",就会覆盖@CacheConfig里的value = "userCache"—— 这个优先级要记清楚。

四、进阶配置:从 “能用” 到 “好用”,这些配置不能少

刚才的例子用的是默认配置,但生产环境里肯定不够用。比如默认的 Redis 缓存序列化方式是 JDK 序列化,存到 Redis 里的是一堆乱码;默认没有缓存过期时间,数据会一直存在 Redis 里,占内存;还有不同的业务可能需要不同的缓存策略 —— 这些都得靠自定义配置来解决。

咱们一步步来,把 Spring Cache 配置得 “好用” 起来。

1. 解决 Redis 缓存乱码:自定义序列化方式

先看个坑:刚才的例子里,你用 Redis 客户端(比如 Redis Desktop Manager)查看缓存,会发现 key 是userCache::1,但 value 是一堆乱码,根本看不懂。这是因为 Spring Cache 默认用的是JdkSerializationRedisSerializer,这种序列化方式虽然能用,但可读性太差,而且占空间。

解决办法很简单:把序列化方式换成Jackson2JsonRedisSerializer,这样存到 Redis 里的是 JSON 格式,又好懂又省空间。

写个 Redis 缓存配置类:

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching// 这里加也行,启动类加也行,只要加一次
publicclass RedisCacheConfig {

    // 自定义Redis缓存管理器
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 1. 配置序列化方式
        // Key的序列化:用StringRedisSerializer,key是字符串
        RedisSerializer<String> keySerializer = new StringRedisSerializer();
        // Value的序列化:用Jackson2JsonRedisSerializer,转成JSON
        Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        
        // 配置Jackson,解决LocalDateTime等Java 8时间类型序列化问题
        com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
        // 开启类型信息,反序列化时能知道对象的类型(避免List等集合反序列化出错)
        objectMapper.activateDefaultTyping(
                com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL,
                com.fasterxml.jackson.databind.jsontype.TypeSerializer.DefaultImpl.NON_FINAL
        );
        // 支持Java 8时间类型(LocalDateTime、LocalDate等)
        objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
        valueSerializer.setObjectMapper(objectMapper);

        // 2. 配置默认的缓存规则(比如默认过期时间30分钟)
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)) // 默认过期时间30分钟
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer)) // 序列化key
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer)) // 序列化value
                .disableCachingNullValues(); // 禁止缓存null值(可选,看业务需求)

        // 3. 配置不同缓存命名空间的个性化规则(比如userCache过期1小时,userListCache过期10分钟)
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("userCache", defaultCacheConfig.entryTtl(Duration.ofHours(1))); // userCache过期1小时
        cacheConfigurations.put("userListCache", defaultCacheConfig.entryTtl(Duration.ofMinutes(10))); // userListCache过期10分钟

        // 4. 创建缓存管理器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultCacheConfig) // 默认规则
                .withInitialCacheConfigurations(cacheConfigurations) // 个性化规则
                .build();
    }
}

再在 application.yml 里配置 Redis 的连接信息(不然连不上 Redis):

spring:
  redis:
    host: localhost # Redis地址,生产环境填实际地址
    port: 6379 # Redis端口
    password: # Redis密码,没有就空着
    database: 0 # Redis数据库索引(默认0)
    lettuce: # Spring Boot 2.x默认用lettuce客户端,比jedis好
      pool:
        max-active: 8 # 最大连接数
        max-idle: 8 # 最大空闲连接数
        min-idle: 2 # 最小空闲连接数
        max-wait: 1000ms # 连接池最大阻塞等待时间

现在再启动项目,调用 getUserById (1),去 Redis 里看:

  • key 还是userCache::1,没变。
  • value 变成了 JSON 格式:{"@class":"com.example.demo.entity.User","id":1,"username":"用户1","phone":"13800138001","createTime":["java.time.LocalDateTime",["2024-05-20T15:30:45.123"]]}—— 虽然多了点类型信息,但至少能看懂了,而且 Java 8 的 LocalDateTime 也能正常序列化 / 反序列化了。

这个配置解决了两个大问题:缓存乱码和时间类型序列化失败,生产环境必备!

2. 自定义 Key 生成策略:不用再手动写 key

刚才的例子里,每个方法都要写key = "#id"、key = "#user.id",要是方法参数多了,key 写起来很麻烦,还容易出错。Spring Cache 允许我们自定义 Key 生成策略,以后不用再手动写 key 了。

比如我们定义一个 “Key 生成器”:key 由 “方法名 + 参数值” 组成,这样能保证唯一性。

写个自定义 KeyGenerator:

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;

@Configuration
publicclass CustomKeyGeneratorConfig {

    // 自定义Key生成器,Bean名称是customKeyGenerator
    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        returnnew KeyGenerator() {
            @Override
            publicObject generate(Object target, Method method, Object... params) {
                // 生成规则:方法名 + 参数列表(比如 getUserById[1])
                String key = method.getName() + "[" + Arrays.toString(params) + "]";
                System.out.println("生成的缓存key:" + key);
                return key;
            }
        };
    }
}

然后在方法上用keyGenerator属性指定用这个生成器,不用再写key了:

@Service
@CacheConfig(value = "userCache", keyGenerator = "customKeyGenerator") // 类级配置,指定Key生成器
public class UserService {

    // 不用写key了,Key生成器会自动生成:getUserById[1]
    @Cacheable
    public User getUserById(Long id) {
        // 逻辑...
    }

    // 自动生成key:updateUser[User(id=1, username=xxx, ...)]
    @CachePut
    public User updateUser(User user) {
        // 逻辑...
    }
}

这样一来,不管方法有多少个参数,Key 生成器都会自动生成唯一的 key,再也不用手动写复杂的 SpEL 表达式了。当然,你也可以根据自己的业务需求修改生成规则,比如加上类名(避免不同 Service 的方法名重复导致 key 冲突),灵活得很。不过要注意:key和keyGenerator不能同时用,用了一个就不能用另一个,不然会报错 —— 这个坑别踩。

3. 多缓存管理器:Redis 和 Ehcache 按需切换

有时候项目里可能需要用多种缓存,比如本地缓存用 Ehcache(快),分布式缓存用 Redis(多实例共享)。Spring Cache 支持配置多个缓存管理器,然后在方法上指定用哪个。

比如我们再配置一个 Ehcache 的缓存管理器:

首先加 Ehcache 的依赖:

<!-- Ehcache 依赖 -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然后写 Ehcache 的配置文件(src/main/resources/ehcache.xml):

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://www.ehcache.org/schema/ehcache-core.xsd"
        updateCheck="false">

    <!-- 本地缓存:默认配置 -->
    <defaultCache
            maxEntriesLocalHeap="1000" <!-- 堆内存最大缓存条目 -->
            eternal="false" <!-- 是否永久有效 -->
            timeToIdleSeconds="60" <!-- 空闲时间(秒),超过这个时间没人用就过期 -->
            timeToLiveSeconds="60" <!-- 存活时间(秒),不管用不用都过期 -->
            memoryStoreEvictionPolicy="LRU"> <!-- 淘汰策略:LRU(最近最少使用) -->
    </defaultCache>

    <!-- 自定义缓存:localCache(本地缓存) -->
    <cache name="localCache"
           maxEntriesLocalHeap="500"
           eternal="false"
           timeToIdleSeconds="30"
           timeToLiveSeconds="30"
           memoryStoreEvictionPolicy="LRU">
    </cache>
</config>

再在配置类里加 Ehcache 的缓存管理器:

import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.ConfigurationFactory;

@Configuration
publicclass MultiCacheManagerConfig {

    // 1. Redis缓存管理器(之前写过,这里省略,注意Bean名称是redisCacheManager)

    // 2. Ehcache缓存管理器(Bean名称是ehcacheCacheManager)
    @Bean("ehcacheCacheManager")
    public CacheManager ehcacheCacheManager() {
        // 加载Ehcache配置文件
        net.sf.ehcache.CacheManager ehcacheManager = CacheManager.create(ConfigurationFactory.parseConfiguration(getClass().getResourceAsStream("/ehcache.xml")));
        returnnew EhCacheCacheManager(ehcacheManager);
    }
}

然后在方法上用cacheManager属性指定用哪个缓存管理器:

@Service
public class UserService {

    // 用Redis缓存管理器(分布式缓存)
    @Cacheable(value = "userCache", cacheManager = "redisCacheManager")
    public User getUserById(Long id) {
        // 逻辑...
    }

    // 用Ehcache缓存管理器(本地缓存,适合不共享的临时数据)
    @Cacheable(value = "localCache", cacheManager = "ehcacheCacheManager")
    public List<String> getLocalTempData() {
        // 模拟获取本地临时数据(比如配置信息,不用共享)
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        returnArrays.asList("temp1", "temp2", "temp3");
    }
}

这样一来,getUserById用 Redis 缓存(多实例部署时能共享),getLocalTempData用 Ehcache 本地缓存(速度快,不占 Redis 资源)—— 按需选择,灵活高效。

五、踩坑指南:这些问题不注意,上线准出幺蛾子

Spring Cache 虽然好用,但要是不注意这些细节,上线了肯定出问题。我整理了几个实战中最容易踩的坑,帮你避坑。

1. 缓存穿透:查不存在的数据,一直打数据库

问题描述:比如有人故意查user.id=-1(数据库里根本没有这个用户),@Cacheable会执行方法,返回 null,默认情况下 null 不会被缓存(除非你配置了enableCachingNullValues())。这样一来,每次查id=-1都会穿透到数据库,要是有人恶意刷这个接口,数据库直接就崩了。

解决方案:

  • 方案一:缓存 null 值。在 Redis 缓存配置里把disableCachingNullValues()改成enableCachingNullValues(),这样查不到的数据也会缓存(value 是 null),下次再查就直接返回 null,不打数据库了。

但要注意:缓存 null 值会占空间,所以要给这类缓存设置较短的过期时间(比如 5 分钟),避免浪费内存。

  • 方案二:参数校验。在 Controller 或 Service 里先判断参数是否合法,比如id <=0直接返回错误,根本不执行查询逻辑。

举个例子:

@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
    // 参数校验:id<=0直接返回错误
    if (id <= 0) {
        return Result.fail("用户id不合法");
    }
    User user = userService.getUserById(id);
    return Result.success(user);
}

这种方式最彻底,从源头阻止无效请求。

2. 缓存击穿:热点数据过期,大量请求打数据库

问题描述:比如某个用户是热点数据(比如网红用户,id=10086),缓存过期的瞬间,有 1000 个请求同时查这个用户,这时候缓存里没有,所有请求都会穿透到数据库,把数据库打垮 —— 这就是缓存击穿。

解决方案:

  • 方案一:热点数据永不过期。对于特别热点的数据,比如首页 Banner、网红用户信息,设置缓存永不过期(entryTtl(Duration.ofDays(365*10))),然后通过定时任务(比如 Quartz)定期更新缓存,这样就不会有过期的瞬间。
  • 方案二:互斥锁。在查询方法里加锁,只有一个请求能去数据库查,查到后更新缓存,其他请求等待锁释放后再查缓存。

举个用 Redis 分布式锁实现的例子(用 Redisson 客户端,比自己写锁简单):

首先加 Redisson 依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version> <!-- 选和Spring Boot兼容的版本 -->
</dependency>

然后修改 Service 方法:

import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
publicclass UserService {

    @Autowired
    private RedissonClient redissonClient;

    @Cacheable(value = "userCache", key = "#id")
    public User getUserById(Long id) {
        // 1. 判断是不是热点数据(这里假设id=10086是热点数据)
        if (id == 10086) {
            // 2. 获取分布式锁(锁的key:userLock:10086)
            String lockKey = "userLock:" + id;
            RLock lock = redissonClient.getLock(lockKey);
            try {
                // 3. 加锁(等待10秒,持有锁30秒,防止死锁)
                boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
                if (locked) {
                    try {
                        // 4. 加锁成功,查数据库(这里模拟)
                        return queryUserFromDb(id);
                    } finally {
                        // 5. 释放锁
                        lock.unlock();
                    }
                } else {
                    // 6. 加锁失败,等待100毫秒后重试(递归调用自己)
                    TimeUnit.MILLISECONDS.sleep(100);
                    return getUserById(id);
                }
            } catch (InterruptedException e) {
                thrownew RuntimeException("获取锁失败");
            }
        } else {
            // 非热点数据,直接查数据库
            return queryUserFromDb(id);
        }
    }

    // 模拟从数据库查数据
    private User queryUserFromDb(Long id) {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        User user = new User();
        user.setId(id);
        user.setUsername("用户" + id);
        user.setPhone("1380013800" + (id % 10));
        user.setCreateTime(LocalDateTime.now());
        return user;
    }
}

这样一来,即使热点数据缓存过期,也只有一个请求能去查数据库,其他请求都等锁释放后查缓存,不会打垮数据库。

3. 缓存雪崩:大量缓存同时过期,数据库被压垮

问题描述:比如你给所有缓存都设置了同一个过期时间(比如凌晨 3 点),到了 3 点,大量缓存同时过期,这时候正好有大量用户访问,所有请求都穿透到数据库,数据库直接扛不住 —— 这就是缓存雪崩。

解决方案:

  • 方案一:过期时间加随机值。给每个缓存的过期时间加个随机数,比如默认 30 分钟,再加上 0-10 分钟的随机值,这样缓存就不会同时过期了。

在 Redis 缓存配置里修改:

import java.util.Random;

// 个性化缓存规则时加随机值
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
Random random = new Random();
// userCache过期时间:1小时 ± 10分钟
long userCacheTtl = 3600 + random.nextInt(600); 
cacheConfigurations.put("userCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheTtl)));
// userListCache过期时间:10分钟 ± 2分钟
long userListCacheTtl = 600 + random.nextInt(120);
cacheConfigurations.put("userListCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userListCacheTtl)));
  • 方案二:分批次过期。把缓存分成多个批次,比如按用户 id 的尾号分 10 批,每批的过期时间差 10 分钟,这样即使一批过期,也只有 10% 的请求会打数据库,压力小很多。
  • 方案三:Redis 集群。用 Redis 集群(主从 + 哨兵)保证 Redis 的高可用,即使某个 Redis 节点挂了,其他节点还能提供缓存服务,避免 Redis 挂了导致所有请求打数据库。

4. 缓存一致性:数据库改了,缓存没更

问题描述:比如你更新了数据库里的用户信息,但缓存里的还是旧数据,这时候用户查到的就是旧数据 —— 这就是缓存一致性问题。这个问题在分布式系统里很常见,比如多实例部署时,实例 A 更新了数据库,但实例 B 的缓存还是旧的。

解决方案:

  • 方案一:更新数据库后立即更新 / 删除缓存。就是咱们之前用的@CachePut(更新缓存)和@CacheEvict(删除缓存),这是最常用的方式,适合大部分场景。

注意:要先更数据库,再更缓存。如果先更缓存,再更数据库,中间有其他请求查缓存,拿到的是新缓存,但数据库还是旧的,会导致不一致。

  • 方案二:延迟双删。如果更新操作比较频繁,或者多实例部署时缓存同步有延迟,可以用 “延迟双删”:先删缓存,再更数据库,过一会儿再删一次缓存。

举个例子:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
publicclass UserService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 延迟双删:更新用户信息
    public User updateUserWithDelayDelete(User user) {
        String cacheKey = "userCache::" + user.getId();

        // 第一步:先删除缓存
        stringRedisTemplate.delete(cacheKey);

        // 第二步:更新数据库
        User updatedUser = updateUserInDb(user);

        // 第三步:延迟1秒后再删除一次缓存(用异步线程,不阻塞主流程)
        delayDeleteCache(cacheKey, 1);

        return updatedUser;
    }

    // 异步延迟删除缓存
    @Async// 要在启动类加@EnableAsync启用异步
    public void delayDeleteCache(String key, long delaySeconds) {
        try {
            TimeUnit.SECONDS.sleep(delaySeconds);
            stringRedisTemplate.delete(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 模拟更新数据库
    private User updateUserInDb(User user) {
        // 数据库更新逻辑...
        return user;
    }
}

为什么要删两次?因为第一步删缓存后,可能有其他实例已经查了数据库(旧数据),正在往缓存里存,这时候第二步更新数据库后,缓存里还是旧数据。延迟 1 秒再删一次,就能把这个旧缓存删掉,保证一致性。

  • 方案三:用 Canal 监听数据库 binlog。如果业务对缓存一致性要求特别高,可以用 Canal 监听 MySQL 的 binlog 日志,当数据库数据变化时,Canal 会触发事件,自动更新 / 删除 Redis 缓存。这种方式比较复杂,但一致性最好,适合大型项目。

六、实战案例:完整的用户服务缓存实现

讲了这么多理论,咱们来个完整的实战案例,把前面的知识点串起来。实现一个用户服务,包含 “查单个用户、查用户列表、更新用户、删除用户” 四个接口,用 Spring Cache+Redis 实现缓存,解决常见问题。

1. 项目结构

com.example.demo
├── config
│ ├── RedisCacheConfig.java// Redis缓存配置
│ └── CustomKeyGeneratorConfig.java// 自定义Key生成器
├── entity
│ └── User.java// 用户实体类
├── service
│ ├── UserService.java// 用户服务接口
│ └── impl
│ └── UserServiceImpl.java// 用户服务实现(带缓存)
├── controller
│ └── UserController.java// 控制层
└── DemoApplication.java  // 启动类

2. 关键代码实现

(1)User.java(实体类)
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String phone;
    private Integer age;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
(2)RedisCacheConfig.java(缓存配置)
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@Configuration
publicclass RedisCacheConfig {

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 1. 配置序列化
        RedisSerializer<String> keySerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
        objectMapper.activateDefaultTyping(
                com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL,
                com.fasterxml.jackson.databind.jsontype.TypeSerializer.DefaultImpl.NON_FINAL
        );
        objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
        valueSerializer.setObjectMapper(objectMapper);

        // 2. 默认缓存配置(过期时间30分钟,加随机值避免雪崩)
        Random random = new Random();
        long defaultTtl = 1800 + random.nextInt(300); // 30分钟 ± 5分钟
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(defaultTtl))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
                .enableCachingNullValues(); // 缓存null值,解决穿透

        // 3. 个性化缓存配置
        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        // userCache:1小时 ± 10分钟
        long userCacheTtl = 3600 + random.nextInt(600);
        cacheConfigs.put("userCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheTtl)));
        // userListCache:10分钟 ± 2分钟
        long userListCacheTtl = 600 + random.nextInt(120);
        cacheConfigs.put("userListCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userListCacheTtl)));

        // 4. 创建缓存管理器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultCacheConfig)
                .withInitialCacheConfigurations(cacheConfigs)
                .build();
    }
}
(3)CustomKeyGeneratorConfig.java(Key 生成器)
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;

@Configuration
publicclass CustomKeyGeneratorConfig {

    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> {
            // 生成规则:类名.方法名[参数列表](避免不同类方法名重复)
            String className = target.getClass().getSimpleName();
            String methodName = method.getName();
            String paramStr = Arrays.toString(params);
            return className + "." + methodName + "[" + paramStr + "]";
        };
    }
}
(4)UserService.java(接口)
import com.example.demo.entity.User;
import java.util.List;

publicinterface UserService {
    // 查单个用户
    User getUserById(Long id);

    // 查用户列表
    List<User> getUserList(Integer pageNum, Integer pageSize);

    // 更新用户
    User updateUser(User user);

    // 删除用户
    void deleteUser(Long id);
}
(5)UserServiceImpl.java(服务实现,带缓存)
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
// 统一配置:缓存命名空间、Key生成器
@CacheConfig(value = "userCache", keyGenerator = "customKeyGenerator")
publicclass UserServiceImpl implements UserService {

    // 模拟数据库(实际项目中是MyBatis/JPA)
    private List<User> mockDb() {
        return Arrays.asList(
                createUser(1L, "张三", "13800138001", 20),
                createUser(2L, "李四", "13800138002", 25),
                createUser(3L, "王五", "13800138003", 30)
        );
    }

    private User createUser(Long id, String username, String phone, Integer age) {
        User user = new User();
        user.setId(id);
        user.setUsername(username);
        user.setPhone(phone);
        user.setAge(age);
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        return user;
    }

    /**
     * 查单个用户:用缓存,解决穿透(缓存null)、击穿(热点数据加锁)
     */
    @Override
    @Cacheable(unless = "#result == null") // 结果为null也缓存(配置里已enableCachingNullValues)
    public User getUserById(Long id) {
        // 模拟热点数据(id=1是热点)
        if (id == 1) {
            // 这里可以加分布式锁,参考前面的缓存击穿解决方案,省略代码
        }

        // 模拟数据库查询耗时
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 从模拟数据库查询
        return mockDb().stream()
                .filter(user -> user.getId().equals(id))
                .findFirst()
                .orElse(null); // 查不到返回null,会被缓存
    }

    /**
     * 查用户列表:用单独的缓存命名空间,避免和单个用户缓存冲突
     */
    @Override
    @Cacheable(value = "userListCache") // 覆盖类上的value,用userListCache
    public List<User> getUserList(Integer pageNum, Integer pageSize) {
        // 模拟数据库查询耗时
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 模拟分页(实际项目用PageHelper)
        int start = (pageNum - 1) * pageSize;
        int end = Math.min(start + pageSize, mockDb().size());
        return mockDb().subList(start, end);
    }

    /**
     * 更新用户:更新缓存,同时删除列表缓存(保证一致性)
     */
    @Override
    @Caching(
        put = {
            @CachePut(key = "#user.id") // 更新单个用户缓存
        },
        evict = {
            @CacheEvict(value = "userListCache", allEntries = true) // 删除列表缓存
        }
    )
    public User updateUser(User user) {
        // 模拟数据库更新耗时
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 模拟更新数据库(实际项目会更新MySQL)
        User updatedUser = createUser(
                user.getId(),
                user.getUsername(),
                user.getPhone(),
                user.getAge()
        );
        updatedUser.setUpdateTime(LocalDateTime.now());
        return updatedUser;
    }

    /**
     * 删除用户:删除缓存,同时删除列表缓存
     */
    @Override
    @Caching(
        evict = {
            @CacheEvict(key = "#id"), // 删除单个用户缓存
            @CacheEvict(value = "userListCache", allEntries = true) // 删除列表缓存
        }
    )
    public void deleteUser(Long id) {
        // 模拟数据库删除耗时
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 模拟删除数据库记录(实际项目会删MySQL)
        System.out.println("用户" + id + "已从数据库删除");
    }
}
(6)UserController.java(控制层)
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/user")
publicclass UserController {

    @Autowired
    private UserService userService;

    // 查单个用户
    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        long start = System.currentTimeMillis();
        User user = userService.getUserById(id);
        long end = System.currentTimeMillis();
        System.out.println("查询耗时:" + (end - start) + "ms");
        return user;
    }

    // 查用户列表(分页)
    @GetMapping("/list")
    public List<User> getList(
            @RequestParam(defaultValue = "1") Integer pageNum,
            @RequestParam(defaultValue = "2") Integer pageSize) {
        long start = System.currentTimeMillis();
        List<User> list = userService.getUserList(pageNum, pageSize);
        long end = System.currentTimeMillis();
        System.out.println("列表查询耗时:" + (end - start) + "ms");
        return list;
    }

    // 更新用户
    @PutMapping
    public User update(@RequestBody User user) {
        return userService.updateUser(user);
    }

    // 删除用户
    @DeleteMapping("/{id}")
    public String delete(@PathVariable Long id) {
        userService.deleteUser(id);
        return"删除成功";
    }
}
(7)DemoApplication.java(启动类)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableCaching // 启用缓存
@EnableAsync // 启用异步(延迟双删用)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

3. 测试验证

启动项目后,用 Postman 测试:

1)查单个用户:GET http://localhost:8080/user/1

  • 第一次耗时约 2000ms,存缓存。
  • 第二次耗时约 10ms,读缓存。
  • 查id=999(不存在),返回 null,缓存 null 值,下次查耗时约 10ms。

2)查用户列表:GET http://localhost:8080/user/list?pageNum=1&pageSize=2

  • 第一次耗时约 2000ms,存缓存到userListCache。
  • 第二次耗时约 10ms,读缓存。

3)更新用户:PUT http://localhost:8080/user,请求体:

{
    "id": 1,
    "username": "张三更新",
    "phone": "13800138001",
    "age": 21
}
  • 执行后,userCache里 id=1 的缓存被更新,userListCache被清空。
  • 再查id=1,拿到更新后的用户;再查列表,耗时约 2000ms(重新存缓存)。

4)删除用户:DELETE http://localhost:8080/user/1

  • 执行后,userCache里 id=1 的缓存被删除,userListCache被清空。
  • 再查id=1,耗时约 2000ms(重新查库并存缓存)。

所有功能都正常,缓存也能正确工作,常见的缓存问题也都有解决方案 —— 这就是一个生产环境可用的 Spring Cache 实现!

七、总结:为什么说 Spring Cache 是优雅的缓存方式?

看到这里,你应该明白为什么我说 Spring Cache 优雅了吧?咱们总结一下:

  1. 代码更简洁:不用手动写 “查缓存、存缓存、删缓存” 的逻辑,一个注解搞定,业务代码更纯粹。
  2. 解耦更彻底:缓存逻辑和业务逻辑完全分开,换缓存中间件(Redis→Ehcache)不用改业务代码。
  3. 配置更灵活:支持自定义序列化、Key 生成器、过期时间,还能多缓存管理器切换,满足各种场景。
  4. 问题有解法:针对缓存穿透、击穿、雪崩、一致性这些常见问题,都有成熟的解决方案,踩坑成本低。

最后给大家一个建议:别再用 HashMap 当缓存,也别再手动写 RedisTemplate 操作缓存了,赶紧把 Spring Cache 用起来。刚开始可能会觉得配置有点麻烦,但一旦上手,你会发现, 这玩意儿是真的香!

AI大模型学习福利

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值