🚀Spring Boot + Redis:构建高效缓存系统
@TOC
前言 🌟
在现代应用程序中,性能是至关重要的因素之一。随着用户请求的增加,数据库负载会随之增加,这时缓存就成了提升系统性能的关键解决方案。而在缓存技术中,Redis 作为一个高效的内存数据存储引擎,已经成为了最流行的选择之一。那么,如何将 Redis 集成到 Spring Boot 中,以构建一个高效的缓存系统呢?别急,今天我们就来详细探讨如何在 Spring Boot 中使用 Redis,实现轻松的数据缓存。
1. 什么是 Redis? 🤔
Redis 是一个开源的内存数据结构存储系统,常用于缓存、消息队列等场景。它支持丰富的数据类型,如字符串、哈希、列表、集合、有序集合等,可以在内存中快速读取和写入数据,这使得 Redis 成为提升系统性能的理想选择。
为什么要使用 Redis 作为缓存?
- 高效快速:Redis 基于内存操作,读写速度非常快,比传统的数据库要高效得多。
- 数据持久化:虽然 Redis 是内存数据库,但它支持数据持久化到磁盘,保证了数据不丢失。
- 丰富的数据类型:Redis 提供了多种数据结构,能够满足各种复杂数据存储需求。
- 分布式支持:Redis 支持集群模式,适合大规模分布式部署。
2. 在 Spring Boot 中集成 Redis 🔄
Spring Boot 提供了对 Redis 的原生支持,通过 spring-boot-starter-data-redis
启动器,开发者可以非常方便地将 Redis 集成到 Spring Boot 项目中。
步骤 1: 引入依赖 🛠️
首先,在 pom.xml
文件中添加 Redis 相关的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId> <!-- Jedis 是 Redis 的客户端 -->
</dependency>
这里使用 spring-boot-starter-data-redis
来简化 Redis 集成。jedis
是 Redis 官方提供的客户端,Spring Data Redis 支持的常见客户端还有 lettuce
,可以根据需求选择使用。
步骤 2: 配置 Redis 连接 📝
接下来,在 application.properties
或 application.yml
中配置 Redis 连接信息:
# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=yourpassword # 如果有密码
spring.redis.database=0 # 使用的数据库索引
spring.redis.timeout=2000 # 连接超时时间,单位毫秒
在默认情况下,Redis 使用 6379 端口,并且没有密码。如果你的 Redis 配置了密码或者使用了不同的端口,记得修改这些配置。
步骤 3: 创建 Redis 配置类 🔧
虽然 Spring Boot 会自动配置 Redis,但有时我们需要对 Redis 连接池等进行个性化配置。此时可以创建一个配置类,手动配置 RedisTemplate
和 StringRedisTemplate
:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置默认的序列化方式
template.setDefaultSerializer(RedisSerializer.json());
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
在这个配置类中,我们通过 RedisTemplate
和 StringRedisTemplate
分别为 Redis 提供了两种不同的操作模板:
RedisTemplate
用于操作 Redis 的各种数据结构,如List
、Set
、Hash
等。StringRedisTemplate
仅用于处理 String 类型的数据,通常用于缓存字符串。
代码解析:
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段代码展示了如何使用 Spring Data Redis 配置 Redis 连接和模板。主要功能是定义了两个 Redis 模板:一个 RedisTemplate<String, Object>
用于处理通用对象类型的数据,另一个 StringRedisTemplate
用于处理 Redis 中的字符串类型数据。
1. RedisConfig
配置类
@Configuration
public class RedisConfig {
// ...
}
@Configuration
注解标记这是一个配置类,Spring 会自动扫描并加载这个类中的 Bean 定义。
2. redisTemplate
Bean
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置默认的序列化方式
template.setDefaultSerializer(RedisSerializer.json());
return template;
}
@Bean
注解标记该方法会向 Spring 容器注册一个 Bean,并作为RedisTemplate
的实例提供给其他组件。RedisTemplate<String, Object>
:这是一个通用的 Redis 模板,用于操作 Redis 中的键值对,其中键是String
类型,值是Object
类型。该模板用于执行 Redis 的基本操作,如set
、get
、delete
等。RedisConnectionFactory
:该对象提供了与 Redis 的连接。通过factory
参数,RedisTemplate
可以连接到 Redis 服务。template.setDefaultSerializer(RedisSerializer.json())
:此行代码指定RedisTemplate
使用JSON
格式进行序列化和反序列化。RedisSerializer.json()
返回一个将对象序列化为 JSON 格式的序列化器。通过这种方式,所有存入 Redis 中的对象都会被自动转换为 JSON 字符串,而从 Redis 中获取时也会反序列化回对象。
3. stringRedisTemplate
Bean
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
StringRedisTemplate
是一个简化版的RedisTemplate
,专门用于操作 Redis 中的字符串数据。RedisConnectionFactory
是 Redis 连接的工厂类,它用于创建 Redis 的连接。StringRedisTemplate
仅支持字符串类型的键和值,因此它适用于只需要操作字符串数据的场景,如简单的键值存储。
4. 配置说明
RedisTemplate
和StringRedisTemplate
都依赖于RedisConnectionFactory
,这是 Redis 的连接工厂,负责创建与 Redis 服务的连接。Spring 会自动配置一个默认的连接工厂,可以通过@EnableAutoConfiguration
来启用 Redis 连接的自动配置。- 通过配置
RedisTemplate
,我们能够定制各种数据类型的序列化方式。此处我们将所有对象序列化为 JSON,这使得我们可以存储任何类型的 Java 对象,且能保证其在 Redis 中以 JSON 格式进行存储。
5. 如何使用
在 Spring 应用中,通过注入这两个模板对象,我们就可以方便地进行 Redis 操作:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void demo() {
// 使用 StringRedisTemplate 操作字符串
stringRedisTemplate.opsForValue().set("key", "value");
String value = stringRedisTemplate.opsForValue().get("key");
// 使用 RedisTemplate 操作对象
MyObject obj = new MyObject("test", 123);
redisTemplate.opsForValue().set("myObject", obj);
MyObject retrievedObj = (MyObject) redisTemplate.opsForValue().get("myObject");
}
小结
- 该配置类提供了对
RedisTemplate
和StringRedisTemplate
的自定义配置。 RedisTemplate
使用了JSON
序列化器,这样我们可以存储任何对象并且将它们转换为 JSON 格式存储在 Redis 中。StringRedisTemplate
提供了一个更简单的接口,专门用于处理 Redis 中的字符串数据。
步骤 4: 使用 Redis 操作缓存 🌈
一切配置好之后,就可以通过 RedisTemplate
在 Spring Boot 中操作 Redis 了。下面是一些常见的 Redis 操作示例。
1. 存储和获取简单数据
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 存储数据
public void setData(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
// 获取数据
public String getData(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
}
在这个示例中,我们通过 StringRedisTemplate
的 opsForValue()
方法实现了最基本的存取操作:通过 set
方法存储数据,通过 get
方法获取数据。
代码解析
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段代码展示了如何使用 Spring Data Redis 来存储和获取 Redis 中的数据。它通过 StringRedisTemplate
来操作 Redis,并提供了简单的 setData
和 getData
方法来存储和读取数据。
1. @Service
注解
@Service
public class RedisService {
@Service
注解用于标记该类是一个 Spring 服务类,它的作用类似于@Component
,用于让 Spring 托管这个类的实例,并将它作为一个 Bean 注入到 Spring 容器中。
2. @Autowired
注解
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
注解表示自动注入依赖。Spring 会自动将StringRedisTemplate
类型的 Bean 注入到RedisService
中。StringRedisTemplate
是 Spring Data Redis 提供的一个用于操作 Redis 字符串数据的模板类。
3. setData
方法
public void setData(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
- 这个方法使用
StringRedisTemplate
来将数据存入 Redis 中。 opsForValue()
获取到操作字符串类型数据的ValueOperations
,该接口提供了很多常见的 Redis 字符串操作方法,如set
、get
、increment
等。set(key, value)
方法将key
和value
存入 Redis。
4. getData
方法
public String getData(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
- 这个方法从 Redis 中获取指定
key
的值,并返回。 opsForValue().get(key)
会返回对应key
的字符串值,如果该key
不存在,则返回null
。
使用方式
在 Spring Boot 应用中,可以通过注入 RedisService
类来进行 Redis 数据的存储和读取。以下是如何使用该服务类的示例:
@Autowired
private RedisService redisService;
public void demo() {
// 存储数据到 Redis
redisService.setData("name", "Alice");
// 获取数据
String value = redisService.getData("name");
System.out.println("Retrieved value: " + value);
}
重点说明
StringRedisTemplate
仅适用于操作 Redis 中的字符串类型数据。如果需要处理其他类型的数据(如对象),应该使用RedisTemplate<String, Object>
。opsForValue()
方法提供了对 Redis 中字符串的操作接口,常用于存储简单的键值对。
小结
RedisService
是一个服务类,封装了 Redis 存取操作,提供了setData
和getData
方法来与 Redis 交互。- 使用了
StringRedisTemplate
来进行 Redis 字符串类型数据的存储与读取。 - 该类可以方便地集成到 Spring Boot 应用中,用于处理缓存、会话存储或简单的键值存储等功能。
2. 使用 RedisTemplate 操作复杂数据结构
Redis 还支持多种数据类型,如 List
、Hash
等,下面是如何通过 RedisTemplate
操作这些数据类型的示例。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RedisTemplateService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 存储对象
public void setObject(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
// 获取对象
public Object getObject(String key) {
return redisTemplate.opsForValue().get(key);
}
// 存储 List
public void setList(String key, List<String> list) {
redisTemplate.opsForList().rightPushAll(key, list);
}
// 获取 List
public List<String> getList(String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
}
代码解析
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段代码展示了如何使用 RedisTemplate
来存储和获取 Redis 中的不同数据类型(如对象、列表等)。与之前的 StringRedisTemplate
示例不同,RedisTemplate
提供了更为通用的方式,支持存储各种类型的数据。
1. @Service
注解
@Service
public class RedisTemplateService {
@Service
注解用于标记RedisTemplateService
类为一个 Spring 服务类,使其能够被 Spring 容器管理。
2. @Autowired
注解
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
注解表示自动注入依赖。这里RedisTemplate<String, Object>
类型的 Bean 被注入到RedisTemplateService
中。RedisTemplate
是一个通用的 Redis 操作类,可以操作 Redis 中的各种数据类型,如字符串、列表、哈希、集合等。
3. setObject
方法
public void setObject(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
- 该方法使用
redisTemplate.opsForValue()
来存储一个对象类型的值。 opsForValue()
提供了对 Redis 字符串类型的操作,尽管这里存储的是一个Object
,RedisTemplate
会自动进行序列化,将对象转换为字节数组存储在 Redis 中。
4. getObject
方法
public Object getObject(String key) {
return redisTemplate.opsForValue().get(key);
}
- 该方法从 Redis 获取存储在
key
下的值,并将其返回。 opsForValue().get(key)
返回的是存储的对象,RedisTemplate
会自动进行反序列化,返回原始的对象类型。
5. setList
方法
public void setList(String key, List<String> list) {
redisTemplate.opsForList().rightPushAll(key, list);
}
- 该方法使用
redisTemplate.opsForList()
来存储一个字符串列表(List<String>
)。 rightPushAll(key, list)
将list
中的所有元素依次压入 Redis 列表的右端。
6. getList
方法
public List<String> getList(String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
- 该方法从 Redis 获取存储在
key
下的列表,并返回该列表的所有元素。 opsForList().range(key, 0, -1)
返回从 Redis 列表中获取的所有元素,其中0
是起始索引,-1
表示获取整个列表。
使用方式
在 Spring Boot 应用中,您可以通过注入 RedisTemplateService
类来进行 Redis 数据的存储和读取。以下是如何使用该服务类的示例:
@Autowired
private RedisTemplateService redisTemplateService;
public void demo() {
// 存储一个对象到 Redis
redisTemplateService.setObject("user", new User("Alice", 30));
// 获取对象
User user = (User) redisTemplateService.getObject("user");
System.out.println("Retrieved user: " + user);
// 存储一个列表到 Redis
List<String> languages = Arrays.asList("Java", "Python", "C++");
redisTemplateService.setList("languages", languages);
// 获取列表
List<String> retrievedLanguages = redisTemplateService.getList("languages");
System.out.println("Retrieved languages: " + retrievedLanguages);
}
注意事项
-
对象存储与序列化:
- 当使用
RedisTemplate
存储对象时,Spring Data Redis 会自动使用默认的序列化机制(JdkSerializationRedisSerializer
)将对象转化为字节数组进行存储。 - 如果希望自定义序列化方式,可以使用
GenericJackson2JsonRedisSerializer
等其他序列化机制。
- 当使用
-
列表存储:
opsForList()
提供了对 Redis 列表的操作支持,可以用来执行如rightPushAll
、range
等操作,操作的是 Redis 的列表类型。
小结
RedisTemplateService
类封装了 Redis 数据存储和读取的基本操作,支持存储对象和列表。- 使用
RedisTemplate<String, Object>
,可以灵活地操作 Redis 中的多种数据类型。 - 通过
opsForValue()
和opsForList()
,分别实现了对象和列表的存储与读取。 - 该类可以帮助你更方便地操作 Redis 中的复杂数据结构。
步骤 5: 缓存注解实现自动缓存 🧩
Spring 提供了一个非常便捷的方式来与 Redis 进行集成,那就是使用缓存注解(@Cacheable
, @CachePut
, @CacheEvict
)。这些注解可以帮助你自动实现缓存操作,无需手动操作 RedisTemplate
。
代码示例
- 启用缓存功能
在 Spring Boot 应用中,只需要在主类或者配置类上加上 @EnableCaching
注解,就可以启用缓存支持:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 使用
@Cacheable
注解
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#userId")
public User getUserById(Long userId) {
// 模拟数据库操作
System.out.println("Fetching user from database...");
return new User(userId, "John Doe");
}
}
在这个示例中,@Cacheable
注解会自动将方法的返回值缓存到 Redis 中,当下次请求相同 userId
时,Spring 会直接从 Redis 中获取数据,而不是重新执行方法。
代码解析:
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段代码演示了如何在 Spring Boot 中使用 缓存 来提高应用程序的性能,具体使用了 @EnableCaching
和 @Cacheable
注解来启用和配置缓存。
代码解析
1. 启用缓存功能
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication
: 这是一个组合注解,包含了@Configuration
,@EnableAutoConfiguration
, 和@ComponentScan
。它表示这是一个 Spring Boot 应用的入口类。@EnableCaching
: 这个注解用于启用 Spring 的缓存功能。它会自动配置 Spring Cache 抽象层,使得在应用中能够使用缓存相关的注解,如@Cacheable
、@CachePut
、@CacheEvict
等。
SpringApplication.run(Application.class, args);
会启动 Spring Boot 应用并自动进行缓存配置。
2. 使用 @Cacheable
注解进行缓存
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#userId")
public User getUserById(Long userId) {
// 模拟数据库操作
System.out.println("Fetching user from database...");
return new User(userId, "John Doe");
}
}
@Service
: 这是一个服务类注解,标识该类是一个服务层组件,Spring 会将它自动注册到应用上下文中。@Cacheable(value = "userCache", key = "#userId")
:@Cacheable
注解表示该方法的返回值会被缓存。具体来说,当方法被调用时,Spring 会检查缓存中是否已有相应的值。如果有,直接返回缓存中的值;如果没有,执行方法并将返回结果存入缓存。value
:缓存的名称,这里是"userCache"
,表示使用名为userCache
的缓存。key
:缓存的 key,"#userId"
表示使用方法参数userId
作为缓存的 key。这样不同的userId
会对应不同的缓存值。
3. 缓存操作
当你调用 getUserById
方法时,第一次调用时会从数据库(模拟的)获取数据,并将结果缓存。之后再调用相同的 userId
时,Spring 会直接从缓存中返回数据,而不再执行数据库查询操作。
流程分析
-
第一次调用
getUserById(1L)
时,"Fetching user from database..."
会被打印出来,说明从数据库获取了数据。同时,这个结果会被缓存,缓存的 key 是1L
,缓存的 value 是User(1L, "John Doe")
。 -
第二次调用
getUserById(1L)
时,Spring 会检查缓存中是否存在 key 为1L
的数据。如果存在,它将直接返回缓存中的值,而不再执行数据库操作,也不会打印"Fetching user from database..."
。 -
如果调用
getUserById(2L)
,缓存中没有该 key,方法会被执行并缓存结果。每个不同的userId
会有独立的缓存。
小结
- 缓存的优势:缓存的主要作用是减少数据库或其他外部资源的访问,从而提升应用的响应速度和性能。通过使用
@Cacheable
注解,Spring 自动为我们处理了缓存的逻辑。 - 缓存存储:可以将缓存存储在内存中(如
ConcurrentMapCache
),也可以将缓存配置为使用外部缓存系统(如 Redis, EhCache, etc.)。可以通过application.properties
或application.yml
文件来配置缓存的存储方式。 - 缓存过期和清理:除了
@Cacheable
注解,Spring 还提供了@CacheEvict
和@CachePut
注解,用于手动控制缓存的过期和更新。
通过这种方式,Spring 提供的缓存注解可以大大简化缓存的使用,使得开发者能够更加专注于业务逻辑的实现。
3. 小结 🏁
Spring Boot 与 Redis 的集成非常简单,通过简单的配置和少量代码,你就可以实现一个高效的缓存系统,显著提升应用的性能。通过 Redis,我们可以有效减轻数据库负担、加速数据访问,特别是在需要频繁读取相同数据的场景中。更重要的是,Spring 提供了注解驱动的缓存方案,使得缓存的管理更加方便,极大地简化了代码。
希望这篇文章能帮助你理解如何将 Redis 集成到 Spring Boot 中,并通过它提升应用的性能。如果你还没试过 Redis,赶快动手实践一下吧,构建一个更高效、更快的应用!🎉
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。
✨️ Who am I?
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。
-End-
- 点赞
- 收藏
- 关注作者
评论(0)