java redis(缓存)

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 的 ZADDZRANGE 命令,几行代码就能搞定复杂的排名逻辑。

📨 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 非常简单:

  1. 引入依赖 :在 pom.xml 中加入 spring-boot-starter-data-redis
  2. 配置连接 :在 application.yml 里写上 Redis 服务器的地址(host)和端口(port,默认 6379)。
  3. 注入使用 :直接在代码里注入 StringRedisTemplateRedisTemplate,就可以像操作 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

  1. 在你的 Spring Boot 启动类上加上 @EnableCaching 注解,开启 Spring 的缓存支持。
  2. 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 缓存。但在高并发场景下,可能会出现一个极端情况:

  1. 线程 A 更新了数据库。
  2. 线程 A 还没来得及删除缓存。
  3. 线程 B 此时来查询,发现缓存是旧的(或者空的),于是把数据库里的旧值又读出来,重新塞回了缓存。
  4. 线程 A 此时才把缓存删掉(或者删失败),导致缓存里永久固化了旧数据。

解决办法就是"延迟双删":

  • 先删除一次缓存。
  • 更新数据库。
  • 休眠几百毫秒(比如 500ms,具体看业务)。
  • 再次删除缓存
    这样就能大概率保证,即使中间有线程把旧数据塞回缓存,也会被第二次删除操作给清掉。

🔄 3. 监听数据库日志(Binlog)异步删除 ------ 大厂主流方案

这是一种更可靠、对业务代码侵入性更低的方案。

  • 原理 :使用像 Canal 这样的中间件,伪装成 MySQL 的一个从节点,实时监听数据库的 Binlog(二进制日志)。一旦检测到数据库里的某条数据发生了变更,Canal 就会自动捕获,并通过消息队列(如 Kafka、RocketMQ)发出一条消息,通知消费端去删除 Redis 里对应的缓存。
  • 优点:业务代码只需要专心更新数据库,完全不用管缓存的事,极大地降低了代码的耦合度,并且能保证数据变更和缓存删除的最终一致性。

🔒 4. 互斥锁(针对热点数据)

对于某些访问量极高、且绝对不能容忍脏数据的"热点 Key"(比如秒杀商品的库存),可以在缓存失效的瞬间,只允许一个线程去查数据库并重建缓存,其他线程排队等待。

  • 实现 :利用 Redis 的分布式锁(SETNX 命令),拿到锁的线程去查库更新,没拿到锁的线程睡一会儿再重试。这虽然牺牲了一点并发性能,但能绝对保证数据的准确性。

总结一下:

Redis 的"数据延迟"确实是它的固有特性,但通过**"TTL兜底 + 延迟双删/监听Binlog"**这套组合拳,完全可以把不一致的时间窗口压缩到用户几乎无感知的程度。这也是为什么在 99% 的互联网业务(如商品详情、用户资料)中,Redis 依然是不可或缺的性能神器。

相关推荐
大大杰哥1 小时前
DAG 学习笔记:从拓扑排序到并行执行
java
Rust研习社1 小时前
Rust 操作 Redis 从入门到生产级应用
开发语言·redis·后端·rust
2501_913061341 小时前
JVM虚拟机——面试中的八股文(下)
java·jvm·面试
京师20万禁军教头1 小时前
36面向对象(高级)-类变量(静态变量)和类方法(静态方法)
java
deviant-ART1 小时前
HttpServletResponse 中 Header 与 OutputStream 的正确使用顺序(避坑指南)
java·后端·servlet
Wmenghu1 小时前
Ubuntu 安装 MySQL 8.0 + Redis 并开启远程访问
redis·mysql·ubuntu
JAVA面经实录9171 小时前
Spring AI 高频开发万能 Prompt 合集 + 生产级工具类
java·人工智能·spring·prompt
JAVA面经实录9171 小时前
如何选择适合项目的「限流 / 熔断 / 降级」方案
java·spring·kafka·sentinel·guava
小雅痞3 小时前
[Java][Leetcode middle] 167. 两数之和 II - 输入有序数组
java·算法·leetcode