java redis的作用
Redis 在 Java 后端开发中,绝对是你进阶路上绕不开的核心中间件。简单来说,它是一个基于内存的超高速数据库。如果把 MySQL 比作硬盘,那 Redis 就是内存,它的读写速度能比传统数据库快几十倍甚至上百倍。
在 Java 项目中,Redis 主要扮演以下几个极其重要的角色:
🚀 1. 高性能缓存(最核心的作用)
这是 Redis 最普遍的用法。将数据库里经常被查询的"热点数据"(比如首页的商品信息、用户的个人资料)提前存到 Redis 内存中。
- 作用 :当用户发起请求时,先查 Redis,查到了直接返回,根本不用去碰数据库。这能极大地减轻数据库的压力,并让接口响应速度达到毫秒级。
- Java 场景 :比如在 Spring Boot 中,查询商品信息时,先去
redisTemplate里拿,拿不到再去查 MySQL,查完后再顺手存一份到 Redis 并设置一个过期时间(比如 10 分钟)。
🔒 2. 分布式锁(解决高并发冲突)
在集群部署的环境下(比如你的 Java 项目启动了多个实例),如何保证同一个资源(比如秒杀活动的库存)在同一时间只能被一个线程修改?
- 作用 :利用 Redis 的原子性操作(如
SETNX命令),可以实现一个全局的"锁"。抢到锁的线程才能执行业务,执行完释放锁,其他线程才能继续抢。 - Java 场景:秒杀系统中防止商品超卖,或者定时任务在多台服务器上只允许一台执行,防止重复执行。
🏆 3. 排行榜与计数器
Redis 提供了非常丰富的数据结构,其中"有序集合(Sorted Set)"天生就是为排行榜设计的。
- 作用:可以极其方便地实现实时积分排名、点赞数统计等。
- Java 场景 :游戏里的玩家战力排行榜、短视频平台的点赞/播放量实时累加。用 Redis 的
ZADD和ZRANGE命令,几行代码就能搞定复杂的排名逻辑。
📨 4. 简单的消息队列与异步解耦
虽然专业的消息队列有 RabbitMQ、Kafka,但 Redis 也能通过"发布/订阅(Pub/Sub)"或列表(List)结构实现轻量级的消息队列。
- 作用:实现业务模块的解耦和异步处理。
- Java 场景:用户下单成功后,Java 程序往 Redis 里丢一条消息,另一个专门负责"发短信"或"记录日志"的后台服务监听到消息后去执行,主流程不用等待这些耗时操作。
🛒 5. 分布式 Session(共享登录状态)
在微服务或多节点部署时,用户登录了 A 服务器,下次请求可能被负载均衡到了 B 服务器,B 服务器怎么知道用户已经登录了?
- 作用:把用户的登录信息(Token、Session)统一存在 Redis 里。
- Java 场景:不管用户请求打到哪台 Java 服务器,大家都去同一个 Redis 里校验用户的登录状态,实现跨服务器的会话共享。
🛠️ Java 中如何引入 Redis?
在 Spring Boot 项目中,引入 Redis 非常简单:
- 引入依赖 :在
pom.xml中加入spring-boot-starter-data-redis。 - 配置连接 :在
application.yml里写上 Redis 服务器的地址(host)和端口(port,默认 6379)。 - 注入使用 :直接在代码里注入
StringRedisTemplate或RedisTemplate,就可以像操作 Map 一样去存取数据了。
一句话总结:
Redis 就是 Java 后端应对高并发、高性能场景的"加速器"和"稳定器"。学会了 Redis,你的系统才能扛得住大流量,这也是从初级迈向高级开发的必经之路。
spring-boot + mabatis-plus,对用户详情接口加入缓存,具体怎么实现
在 Spring Boot + MyBatis-Plus 项目中,给用户详情接口加缓存,最推荐、最优雅的方式是使用 Spring 官方提供的 @Cacheable 注解配合 Redis。
这样做的好处是业务代码和缓存逻辑完全解耦 ,你不需要在 Service 层写一大堆 redisTemplate.get() 或 redisTemplate.set() 的样板代码,Spring 会自动帮你处理"有缓存直接返回,没缓存查库并写入"的逻辑。
以下是具体的实现步骤:
🛠️ 第一步:引入 Redis 依赖
在你的 pom.xml 中引入 Spring Boot 的 Redis 启动器:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
⚙️ 第二步:开启缓存并配置 Redis
- 在你的 Spring Boot 启动类上加上
@EnableCaching注解,开启 Spring 的缓存支持。 - 在
application.yml中配置好你的 Redis 连接信息:
yaml
spring:
redis:
host: 127.0.0.1
port: 6379
# password: your_password # 如果有密码就填
database: 0
💼 第三步:在 Service 层使用缓存注解(核心)
在查询用户详情的方法上加上 @Cacheable,在修改和删除用户的方法上加上 @CacheEvict(用于在数据变更时清除缓存,防止脏数据)。
java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 查询用户详情(加入缓存)
* value = "user":代表 Redis 中的命名空间(前缀)
* key = "#id":代表缓存的 key 是方法的参数 id,最终在 Redis 里的 key 是 user::1
*/
@Cacheable(value = "user", key = "#id")
public User getUserById(Long id) {
System.out.println("缓存未命中,正在查询数据库..."); // 用来测试是否走了数据库
return userMapper.selectById(id);
}
/**
* 更新用户信息(清除对应缓存)
* 当用户信息被修改时,必须把旧缓存删掉,否则下次查到的还是旧数据
*/
@CacheEvict(value = "user", key = "#user.id")
public boolean updateUser(User user) {
return userMapper.updateById(user) > 0;
}
/**
* 删除用户(清除对应缓存)
*/
@CacheEvict(value = "user", key = "#id")
public boolean deleteUser(Long id) {
return userMapper.deleteById(id) > 0;
}
}
🛡️ 第四步:配置缓存的过期时间(TTL)
为了防止缓存一直不过期导致内存溢出,或者出现数据长期不一致的情况,建议给缓存设置一个合理的过期时间(比如 30 分钟)。
你可以写一个简单的配置类来全局设置 Redis 缓存的过期时间:
java
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 配置缓存的序列化方式以及默认的过期时间(这里设置为30分钟)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // 不缓存空值
return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();
}
}
💡 补充说明:为什么不推荐用 MyBatis-Plus 自带的二级缓存?
虽然 MyBatis-Plus 也有自带的二级缓存机制(在 Mapper 上加 @CacheNamespace),但在实际企业开发中,极少有人用它。
- 原因:MyBatis-Plus 自带的二级缓存非常"傻瓜式",它会把该 Mapper 下所有的查询结果一股脑缓存起来,很难针对单个接口做精细化的控制(比如只想缓存详情接口,不想缓存列表分页接口)。而且它在分布式环境下如果不手动对接 Redis 实现类,很容易出现"缓存孤岛"和数据不一致的问题。
总结:
使用 Spring Boot 的 @Cacheable 是最标准、最灵活的做法。它完美契合了你刚才提到的"缓存与数据库不一致"的痛点,通过 @CacheEvict 配合,轻松实现了**"查询走缓存,修改删缓存"**的最佳实践。
感觉配置缓存也挺容易的呀
哈哈,没错!在 Spring Boot 里用注解把缓存跑通,确实就是"几分钟"的事情,这完全得益于 Spring 框架的强大封装。
不过,在实际的企业级开发中,我们常说**"缓存入门容易,精通难"**。刚才那几步只是完成了"缓存 1.0"的基础建设,真正考验后端功力的,是当系统流量上来之后,如何应对各种极端情况。
为了让你对缓存有更全面的认识,这里给你透个底,聊聊缓存在实际生产中容易遇到的几个"深坑":
1. 缓存穿透、击穿与雪崩(高并发三座大山)
- 缓存穿透 :如果有黑客故意请求一个数据库里根本不存在 的数据(比如 id = -1),缓存肯定没有,请求就会全部打到数据库上。如果这种请求每秒几万次,数据库可能直接就挂了。
- 解法:即使数据库查不到,也给这个空结果设置一个短暂的缓存(比如缓存 1 分钟的空值),或者使用布隆过滤器提前拦截。
- 缓存击穿 :某个极其热点的数据 (比如秒杀活动的商品详情)缓存突然过期失效了,瞬间成千上万的请求同时打到数据库,导致数据库压力骤增。
- 解法 :对于这种热点数据,可以不设置过期时间,或者在缓存即将失效时,通过分布式锁只让一个线程去查库重建缓存。
- 缓存雪崩 :大量缓存在同一时间集体过期 ,或者 Redis 服务器直接宕机了。这时候所有请求都会像洪水一样涌向数据库。
- 解法 :给缓存的过期时间加上随机数(比如 30分钟 + 随机 1~5分钟),让缓存失效的时间错开;同时做好 Redis 的高可用集群(主从、哨兵模式)。
2. 内存成本与淘汰策略
Redis 是基于内存的,而内存是非常昂贵的资源。你不能无限制地把所有数据都往 Redis 里塞。
- 当 Redis 的内存被占满时,它会根据你配置的淘汰策略(比如 LRU - 最近最少使用)自动把一些不常用的数据踢出去。
- 所以在设计缓存时,必须精打细算:哪些数据值得放进缓存?缓存多大?过期时间设多久?这都是需要结合业务去权衡的。
3. 数据一致性的极致追求
刚才我们提到了"延迟双删"和"监听 Binlog",这其实已经属于进阶操作了。在金融、支付等对数据一致性要求极高的场景下,如何保证 Redis 和 MySQL 的数据绝对一致,是一个非常复杂且容易出 Bug 的领域。有时候为了绝对的安全,甚至不得不牺牲一部分缓存带来的性能优势。
4. 缓存的"双刃剑"效应
不光是后端,其实前端浏览器缓存、操作系统缓存也是一样的道理。比如我们平时觉得电脑或手机卡了,第一反应是"清缓存"。但实际上,频繁手动清理缓存反而会导致系统变慢,因为系统不得不重新去硬盘或网络上拉取数据,增加了硬件损耗和响应延迟。缓存的本质,就是用空间换时间,只要用得好,它就是性能神器。
总结一下:
你现在的状态非常好,已经掌握了缓存的"招式"。接下来如果在实际做项目时,可以多思考一下:"如果有一百万人同时访问这个接口会怎样?" 带着这种问题去查阅资料、优化代码,你很快就能从"会用缓存"进阶到"精通缓存架构"了!加油!
但Redis的弊端是,不会拿到最新的数据,这样用户觉得有问题把
你的直觉非常敏锐!确实,引入 Redis 缓存后,如果处理不好,很容易出现数据库已经更新了,但 Redis 里还是旧数据的情况,这就是经典的**"缓存与数据库数据不一致"**问题。
不过在实际的高并发系统中,我们通常不会追求绝对的、强的一致性(因为那会严重牺牲性能),而是追求**"最终一致性"**。也就是说,允许数据在极短的时间内(比如几百毫秒)存在差异,但保证在用户可接受的时间范围内,数据最终会回到正确的状态。
在 Java 后端开发中,我们有几套非常成熟的标准打法来解决这个问题:
🛡️ 1. 设置过期时间(TTL)------ 最基础的兜底方案
这是最简单也最常用的保底手段。在给 Redis 写入数据时,顺手给它设置一个合理的过期时间(比如 10 分钟、30 分钟)。
- 原理:就算某次数据库更新后,缓存删除失败了,导致用户读到了脏数据。但因为有过期时间,最坏的结果也只是这部分旧数据在缓存里多待了几分钟,时间一到它会自动失效,下次请求就会重新从数据库拉取最新数据。
- 优化技巧 :为了防止大量缓存在同一时间集体过期导致数据库压力骤增(即缓存雪崩 ),我们在设置过期时间时,通常会加一个随机数(比如在 10 分钟的基础上,随机增加 1~300 秒)。
🗑️ 2. 延迟双删 ------ 解决并发下的"幽灵写入"
在更新数据时,标准的流程是:先更新数据库,再删除 Redis 缓存。但在高并发场景下,可能会出现一个极端情况:
- 线程 A 更新了数据库。
- 线程 A 还没来得及删除缓存。
- 线程 B 此时来查询,发现缓存是旧的(或者空的),于是把数据库里的旧值又读出来,重新塞回了缓存。
- 线程 A 此时才把缓存删掉(或者删失败),导致缓存里永久固化了旧数据。
解决办法就是"延迟双删":
- 先删除一次缓存。
- 更新数据库。
- 休眠几百毫秒(比如 500ms,具体看业务)。
- 再次删除缓存 。
这样就能大概率保证,即使中间有线程把旧数据塞回缓存,也会被第二次删除操作给清掉。
🔄 3. 监听数据库日志(Binlog)异步删除 ------ 大厂主流方案
这是一种更可靠、对业务代码侵入性更低的方案。
- 原理 :使用像 Canal 这样的中间件,伪装成 MySQL 的一个从节点,实时监听数据库的 Binlog(二进制日志)。一旦检测到数据库里的某条数据发生了变更,Canal 就会自动捕获,并通过消息队列(如 Kafka、RocketMQ)发出一条消息,通知消费端去删除 Redis 里对应的缓存。
- 优点:业务代码只需要专心更新数据库,完全不用管缓存的事,极大地降低了代码的耦合度,并且能保证数据变更和缓存删除的最终一致性。
🔒 4. 互斥锁(针对热点数据)
对于某些访问量极高、且绝对不能容忍脏数据的"热点 Key"(比如秒杀商品的库存),可以在缓存失效的瞬间,只允许一个线程去查数据库并重建缓存,其他线程排队等待。
- 实现 :利用 Redis 的分布式锁(
SETNX命令),拿到锁的线程去查库更新,没拿到锁的线程睡一会儿再重试。这虽然牺牲了一点并发性能,但能绝对保证数据的准确性。
总结一下:
Redis 的"数据延迟"确实是它的固有特性,但通过**"TTL兜底 + 延迟双删/监听Binlog"**这套组合拳,完全可以把不一致的时间窗口压缩到用户几乎无感知的程度。这也是为什么在 99% 的互联网业务(如商品详情、用户资料)中,Redis 依然是不可或缺的性能神器。