Spring Boot 项目中 Redis 与数据库性能对比实战:从缓存配置到时间分析,详解最佳实践

一、前言:

在现代应用中,随着数据量的增大和访问频率的提高,如何提高数据存取的性能变得尤为重要。缓存技术作为一种常见的优化手段,被广泛应用于减少数据库访问压力、提升系统响应速度。Redis 作为一种高效的内存缓存数据库,因其卓越的性能和丰富的数据类型支持,在开发中占据了重要位置。

本篇文章将详细介绍如何在 Spring Boot 项目中使用 Redis,创建简单的缓存机制,并实现数据存取的时间比较。通过这个实战项目,你将学习如何在 Redis 和 MySQL 之间存取数据,并测量两者的性能差异,从而对缓存策略有更加深入的理解。

二、详细操作:

2.1、环境准备和项目结构

首先,你需要准备一个 Spring Boot 项目,并确保项目结构中包含以下依赖:

  • Spring Web:用于构建 RESTful API

  • MyBatis:数据库操作框架

  • Spring Data Redis:用于 Redis 操作

  • MySQL Driver:用于与 MySQL 数据库交互

    <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
          <dependency>
              <groupId>com.mysql</groupId>
              <artifactId>mysql-connector-j</artifactId>
              <scope>runtime</scope>
          </dependency>
    
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-test</artifactId>
              <scope>test</scope>
          </dependency>
          <!-- MyBatis Starter -->
          <dependency>
              <groupId>org.mybatis.spring.boot</groupId>
              <artifactId>mybatis-spring-boot-starter</artifactId>
              <version>3.0.0</version>
          </dependency>
          <!-- Spring Boot Redis Starter -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-data-redis</artifactId>
          </dependency>
          
          <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <version>1.18.30</version>
          </dependency>
      </dependencies>
    

同时项目结构如下,是一个非常普通的SpringBoot项目

同时还要在application.properties中配置好对应的MySQL和MyBatis信息:

spring.datasource.url=jdbc:mysql://localhost:3306/yourDatabase
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/*.xml

2.2、创建实体类

实体类 User 用于表示用户的基本信息,包含 idname 两个字段。代码如下:

@Data
public class User {
    private Long id;
    private String name;
}

2.3、配置MyBatis Mapper层

创建一个接口 UserMapper 来操作数据库,并通过 MyBatis 的 XML 文件实现具体的 SQL 操作:

@Mapper
public interface UserMapper {
    User selectUserById(Long id);
}

<mapper namespace="com.example.redis.mapper.UserMapper">
    <select id="selectUserById" resultType="com.example.redis.entity.User">
        SELECT id, name FROM user WHERE id = #{id}
    </select>
</mapper>

2.4、Redis配置与工具类

自定义的 Redis 配置类 RedisConfig,它基于 Jackson 的 JSON 序列化器进行 Redis 数据存储与读取,确保数据可以以对象形式保存到 Redis 中。(这个配置类和工具类是在网上找的)配置如下:

@Configuration
public class RedisConfig {
    // 自己定义了一个RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 我们为了自己开发方便,一般直接使用 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

工具类 RedisUtil 提供了简单的 Redis 操作方法,例如设置键值对、获取缓存、检查键是否存在等:

package com.example.redis.utils;

import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    public Set<String> keys(String keys){
        try {
            return redisTemplate.keys(keys);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
     /**
     * 普通缓存放入, 不存在放入,存在返回
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean setnx(String key, Object value) {
        try {
            redisTemplate.opsForValue().setIfAbsent(key,value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

  /**
     * 普通缓存放入并设置时间,不存在放入,存在返回
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean setnx(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0){
                expire(key, time);
            }
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0){
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     *
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0){
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

2.5、Service层实现

UserService 中,创建数据时不仅将数据写入数据库,还将其缓存到 Redis 中。查询数据时,优先从 Redis 中获取,若 Redis 中不存在,则查询数据库并缓存结果。还会记录并输出 Redis 和数据库的查询时间:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisUtil redisUtil;

    public User  getUserById(Long id){
        //先从Redis中读取数据
        String key = "user:"+id;
        //判断该数据是否在Redis中
        long redisStartTime = System.currentTimeMillis();
        Object testUser = redisUtil.hasKey(key)?redisUtil.get(key):null;//先判断是否有,有就得到,没有就返回null
        long redisEndTime = System.currentTimeMillis();
        //存在的话则返回数据
        if(testUser!=null){
            System.out.println("Redis 查询时间: " + (redisEndTime - redisStartTime) + " ms");
            return (User) testUser;
        }
        //不存在的话则从数据库中读取数据并将数据保存到Redis中
        long dbStartTime = System.currentTimeMillis();
        User user = userMapper.selectUserById(id);
        long dbEndTime = System.currentTimeMillis();
        System.out.println("数据库查询时间: " + (dbEndTime - dbStartTime) + " ms");

        redisUtil.set(key,user);
        return user;
    }

}

2.6、Controller层实现

通过 UserController 提供 RESTful 接口,用于创建用户和查询用户信息:

@RestController
@RequestMapping("/test")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User test1(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}

三、Redis相关知识点整理:

1. Redis 在缓存中的作用

在本项目中,Redis 主要用于缓存用户数据。通常,在频繁的数据库读操作中,每次查询数据库都会耗费一定的时间和资源。而 Redis 作为一个基于内存的缓存,可以极大地减少对数据库的直接访问,提升系统性能。

在下面我展示一下使用数据库与Redis查询数据的时间对比,还是很明显的,使用Redis能很大程度地减少查询时间

2. Redis 与数据库的数据存取逻辑

UserService 中,我们实现了从 Redis 获取数据的逻辑:

Object cachedUser = redisUtil.hasKey(key) ? redisUtil.get(key) : null;

这里,我们首先判断 Redis 中是否已经缓存了对应用户数据(通过 redisUtil.hasKey(key))。如果缓存存在,则直接从 Redis 中获取数据,从而避免了数据库查询的延时。如果缓存不存在,则执行数据库查询,并将查询结果存入 Redis:

User user = userMapper.selectUserById(id);
redisUtil.set(key, user);

这种 "缓存穿透" 的模式,确保了只有在 Redis 缓存未命中的情况下才会访问数据库,从而实现了高效的数据查询。

3. Redis 数据的存储与序列化

在 Redis 中存储复杂数据类型(如 Java 对象)时,使用了 Jackson2JsonRedisSerializer 来序列化对象:

Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);

这段代码确保了 Redis 可以将 User 对象序列化为 JSON 字符串并存储在 Redis 中,当需要从缓存中获取数据时,再反序列化为 Java 对象。

四、总结:

在本篇文章中,我们通过一个简单的 Spring Boot 项目,结合 Redis 和 MySQL 实现了数据的存取,并比较了它们在时间上的差异。通过 Redis 的引入,可以显著提升系统的性能,特别是在频繁读写的场景下,缓存策略能够有效减轻数据库的压力。

本项目展示了如何使用 Redis 来优化应用性能,以及 Redis 在现代应用架构中的重要性。如果你正在构建一个需要高性能、低延迟的应用,Redis 绝对是你不可或缺的技术之一。

如果这篇文章有帮助到你的话,就点个赞和关注吧,你的鼓励是我最大的动力!

相关推荐
番茄电脑全能王19 分钟前
《鸣潮》运行时提示找不到emp.dll是什么原因?缺失emp.dll文件要怎么解决?
网络·数据库·经验分享·游戏·电脑
苹果酱05671 小时前
Golang的文件解压技术研究与应用案例
java·vue.js·spring boot·mysql·课程设计
一只淡水鱼661 小时前
【mybatis】基本操作:详解Spring通过注解和XML的方式来操作mybatis
java·数据库·spring·mybatis
张声录11 小时前
【ETCD】【实操篇(十六)】基于角色的访问控制:ETCD 安全管理指南
数据库·安全·etcd
warrah1 小时前
redis——岁月云实战
数据库·redis·缓存
秀儿y1 小时前
Redis-十大数据类型
数据库·redis·缓存·oracle
凡人的AI工具箱2 小时前
每天40分玩转Django:Django类视图
数据库·人工智能·后端·python·django·sqlite
路在脚下@2 小时前
MySQL的索引失效的原因有那些
数据库·mysql
凡人的AI工具箱2 小时前
每天40分玩转Django:实操图片分享社区
数据库·人工智能·后端·python·django
CT随2 小时前
MongoDB
数据库·mongodb