SpringBoot整合Redis实现常用功能超详细过程

目录
  • 1 登陆功能
    • 1.1 基于Session实现登录流程
      • 1.1.1 session共享问题
    • 1.2 Redis替代Session
      • 1.2.1、设计key的结构
      • 1.2.2、设计key的具体细节
      • 1.2.3、整体访问流程
  • 2 缓存功能
    • 2.1 什么是缓存?
      • 2.1.1 为什么要使用缓存
      • 2.1.2 如何使用缓存
    • 2.2.使用缓存
      • 2.2.1 、缓存模型和思路
    • 2.3 缓存更新策略
      • 2.3.1 、数据库缓存不一致解决方案:
      • 2.3.2 、数据库和缓存不一致采用什么方案
    • 2.4 缓存穿透问题的解决思路
  • 3.工具类

SpringBoot整合Redis实现常用功能建议大小伙们,在写业务的时候,提前画好流程图,思路会清晰很多。文末有解决缓存穿透和击穿的通用工具类。

1 登陆功能

我想,登陆功能是每个项目必备的功能吧,但是想设计好,却是很难!下面介绍两种登陆功能的解决方式:

  • 基于Session实现登录流程
  • 基于Redis实现登录流程

1.1 基于Session实现登录流程

功能流程:

发送验证码:

  • 用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
  • 如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

短信验证码登录、注册:

  • 用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,
  • 如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息

校验登录状态:

  • 用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则
  • 将用户信息保存到threadLocal中,并且放行

1.1.1 session共享问题

基于session方式实现登陆功能,最大的缺点就是在多台tomcat下session无法共享,就会下出现下面问题。

核心思路分析:

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了

但是这种方案具有两个大问题

1、每台服务器中都有完整的一份session数据,服务器压力过大。

2、session拷贝数据时,可能会出现延迟

所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了

1.2 Redis替代Session

1.2.1、设计key的结构

首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。

1.2.2、设计key的具体细节

所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code了

在设计这个key的时候,我们之前讲过需要满足两点:

1、key要具有唯一性2、key要方便携带

如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了.

1.2.3、整体访问流程

当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。

2 缓存功能

2.1 什么是缓存?

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码(例如:

例1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并发

例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存

例3:Static final Map<K,V> map =  new HashMap(); 本地缓存

由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;

2.1.1 为什么要使用缓存

一句话:因为速度快,好用

缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力

实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术;

但是缓存也会增加代码复杂度和运营的成本:

2.1.2 如何使用缓存

实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用

浏览器缓存:主要是存在于浏览器端的缓存

应用层缓存:可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存

数据库缓存:在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中

CPU缓存:当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存

2.2.使用缓存

2.2.1 、缓存模型和思路

标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis

2.3 缓存更新策略

缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。

内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存

主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

2.3.1 、数据库缓存不一致解决方案:

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:

用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;怎么解决呢?有如下几种方案

Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案(一般采用

Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理

Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

2.3.2 、数据库和缓存不一致采用什么方案

综合考虑使用方案一,但是方案一调用者如何处理呢?这里有几个问题

操作缓存和数据库时有三个问题需要考虑:

如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来

  • 删除缓存还是更新缓存?
  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
  • 如何保证缓存与数据库的操作的同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案

应该具体操作缓存还是操作数据库,我们应当是先操作数据库,再删除缓存,原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

  • 先操作缓存还是先操作数据库?
  • 先删除缓存,再操作数据库(存在线程安全问题)
  • 先操作数据库,再删除缓存

2.4 缓存穿透问题的解决思路

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

  • 缓存空对象
  • 优点:实现简单,维护方便
  • 缺点:
  • 额外的内存消耗
  • 可能造成短期的不一致
  • 布隆过滤
  • 优点:内存占用较少,没有多余key
  • 缺点:
  • 实现复杂
  • 存在误判可能

缓存空对象思路分析:当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了.

布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中

假设布隆过滤器判断这个数据不存在,则直接返回

这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

小总结:

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

3.工具类

此工具类已经对缓存穿透,和缓存击穿实现了通用功能。

可以对比上叙的流程图查阅

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;

/**
 * @author : look-word
 * 2022-08-19 17:02
 **/
@Component
public class CacheClient {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 设置逻辑过期时间
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // .封装逻辑时间
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        String redisDataJson = JSONUtil.toJsonStr(redisData);
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, redisDataJson);
    }

    /**
     * 解决缓存穿透 对未存在的数据 设置为null
     */
    public <R, ID> R queryWithPassThrough
    (String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long cacheTime, TimeUnit cacheUnit) {
        // 缓存key
        String key = keyPrefix + id;
        // 1 查询缓存中是否命中
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        // 解决缓存穿透 数据库不存在的数据 缓存 也不存在 恶意请求
        if (json != null) {
            return null;
        }

        // 2 查询数据库 存在 存入缓存 返回给前端
        R r = dbFallback.apply(id);
        if (r == null) {
            // 解决缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 2.1 转换成json 存入缓存中
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), cacheTime, cacheUnit);

        return r;
    }

    // 线程池
    public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 解决缓存击穿 逻辑过期时间方式
     */
    public <R, ID> R queryWithLogicalExpire
    (String keyPrefix, ID id, Class<R> type, String lockKeyPrefix, Function<ID, R> dbFallback, Long expiredTime, TimeUnit expiredUnit) {
        // 缓存key
        String key = keyPrefix + id;
        // 1 查询缓存中是否命中
        String redisDataJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(redisDataJson)) {
            return null;
        }
        // 2.命中 查看是否过期,
        //     2.1 未过期 直接返回旧数据
        //     2.2 过期 获取锁 查询数据写入Redis设置新的过期时间
        //     2.3 过期 未获取锁 返回 旧数据
        RedisData redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        if (LocalDateTime.now().isBefore(expireTime)) {
            return r;
        }
        String lockKey = lockKeyPrefix + id;
        // 获取锁
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 存储Redis 设置逻辑过期 过期时间
                    setWithLogicalExpire(key, r1, expiredTime, expiredUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 未获取到锁
        return r;
    }

    /**
     * 获取锁
     */
    public boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 100, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    public void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

}

到此这篇关于SpringBoot整合Redis实现常用功能超详细过程的文章就介绍到这了,更多相关SpringBoot整合Redis内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • springboot整合redis之消息队列

    目录 一.项目准备 二.配置类 三.redis中list数据类型 定时器监听队列 运行即监控队列 四.发布/订阅模式 五.ZSet实现延迟队列 一.项目准备 依赖 <!-- RedisTemplate --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> &

  • Springboot框架整合添加redis缓存功能

    目录 一:安装Redis 二:添加Redis依赖 三:添加Redis配置信息 四:创建RedisConfigurer 五:创建Redis常用方法 六:接口测试 Hello大家好,本章我们添加redis缓存功能 .另求各路大神指点,感谢 一:安装Redis 因本人电脑是windows系统,从https://github.com/ServiceStack/redis-windows下载了兼容windows系统的redis 下载后直接解压到自定义目录,运行cmd命令,进入到这个文件夹,在这个文件夹下运

  • SpringBoot整合Mysql和Redis的详细过程

    目录 一.项目创建 1.1创建项目 1.2目录结构 1.3pom.xml配置文件 二.初始化数据库 三.初始化代码 3.1实体类entity 3.2Mapper接口类 3.3Redis工具类 3.4Service层 3.5Controller层 四.单元测试 4.1Respository和Service层单元测试 4.2Controller层接口测试 一.项目创建 1.1 创建项目 在IDEA中,File--New--Project--Spring Initializer 名称为springbo

  • SpringBoot整合Redis之编写RedisConfig

    编写RedisConfig首先我们要明白RedisConfig中需要包含什么,首先看看我们直接使用RedisTemplate的问题,我们就知道RedisConfig要包含什么了,我们在RedisConfig需要规定好根据不同的查询生成的key,key和value的序列化和反序列化 我们在RedisConfig中配置的自定义方法,最终通过注解引用的就是我们自定义的方法 RedisConfig.java   : 对redis提供的两个Template的序列化配置 /** * @Author: JCc

  • SpringBoot整合redis中的JSON序列化文件夹操作小结

    目录 前言 快速配置 JSON序列化 jackson序列化 Fastjson序列化 分析参考对比 更多问题参考 redis数据库操作 前言 最近在开发项目,用到了redis作为缓存,来提高系统访问速度和缓解系统压力,提高用户响应和访问速度,这里遇到几个问题做一下总结和整理 快速配置 SpringBoot整合redis有专门的场景启动器整合起来还是非常方便的 <dependency> <groupId>org.springframework.boot</groupId>

  • SpringSecurity整合springBoot、redis实现登录互踢功能

    背景 基于我的文章--<SpringSecurity整合springBoot.redis token动态url权限校验>.要实现的功能是要实现一个用户不可以同时在两台设备上登录,有两种思路: (1)后来的登录自动踢掉前面的登录. (2)如果用户已经登录,则不允许后来者登录. 需要特别说明的是,项目的基础是已经是redis维护的session. 配置redisHttpSession 设置spring session由redis 管理. 2.1去掉yml中的http session 配置,yml和

  • SpringBoot整合Redis实现常用功能超详细过程

    目录 1 登陆功能 1.1 基于Session实现登录流程 1.1.1 session共享问题 1.2 Redis替代Session 1.2.1.设计key的结构 1.2.2.设计key的具体细节 1.2.3.整体访问流程 2 缓存功能 2.1 什么是缓存? 2.1.1 为什么要使用缓存 2.1.2 如何使用缓存 2.2.使用缓存 2.2.1 .缓存模型和思路 2.3 缓存更新策略 2.3.1 .数据库缓存不一致解决方案: 2.3.2 .数据库和缓存不一致采用什么方案 2.4 缓存穿透问题的解决

  • Springboot整合Redis实现超卖问题还原和流程分析(分布式锁)

    目录 超卖简单代码 超卖问题 单服务器单应用情况下 设置synchronized Redis实现分布式锁 通过超时间解决上述问题 通过key设置值匹配的方式解决形同虚设问题 最终版 超卖简单代码 写一段简单正常的超卖逻辑代码,多个用户同时操作同一段数据,探究出现的问题. Redis中存储一项数据信息,请求对应接口,获取商品数量信息: 商品数量信息如果大于0,则扣减1,重新存储Redis中: 运行代码测试问题. /** * Redis数据库操作,超卖问题模拟 * @author * */ @Res

  • Springboot整合Redis的详细教程分享

    目录 1.Docker 安装 Redis 1.1 下载镜像 1.2 创建配置文件 1.3 启动Redis 1.4 进入Redis容器 1.5 redis 可视化工具 2.SpringBoot 整合Redis缓存 2.1 安装Redis 2.2 引入依赖 2.3 配置Redis地址端口 2.4 测试 1.Docker 安装 Redis 1.1 下载镜像 docker pull redis:6.2.6 1.2 创建配置文件 mkdir -p /mydata/redis/conf touch /myd

  • SpringBoot整合redis及mongodb的详细过程

    目录 一.先看Redis的使用: 1. 在pom.xml中添加Redis相关依赖项 2. 在application.properties中添加Redis的相关配置 3. 新建 service/RedisService 接口及其实现类 service/impl/RedisServiceImpl 4. 新建 controller/RedisController 5. 通过Postman进行结果验证 二.MongoDB的使用 1. 首先还是先添加MongoDB相关依赖项 2. 然后是添加MongoDB

  • SpringBoot整合Redis、ApachSolr和SpringSession的示例

    本文介绍了SpringBoot整合Redis.ApachSolr和SpringSession,分享给大家,具体如下: 一.简介 SpringBoot自从问世以来,以其方便的配置受到了广大开发者的青睐.它提供了各种starter简化很多繁琐的配置.SpringBoot整合Druid.Mybatis已经司空见惯,在这里就不详细介绍了.今天我们要介绍的是使用SpringBoot整合Redis.ApacheSolr和SpringSession. 二.SpringBoot整合Redis Redis是大家比

  • SpringBoot整合Redis的示例

    redis是最常用的缓存数据库,常用于存储用户登录token.临时数据.定时相关数据等. redis是单线程的,所以redis的操作是原子性的,这样可以保证不会出现并发问题. redis基于内存,速度非常快,据测试,redis读的速度是110000次/s,写的速度是81000次/s 本节介绍SpringBoot引入redis,以及使用RedisTemplate来操作redis数据. 采用SpringBoot 2.1.9.RELEASE,对应示例代码在:https://github.com/lao

  • SpringBoot整合Redis的步骤

    1.添加配置文件: Redis.properties # 配置单台redis服务器 redis.host=192.168.126.129 ip地址 redis.port=6379 端口号 设置配置类: RedisConfig 2.将对象转化为Json格式入门案例 API: MAPPER.writeValueAsString(itemDesc); 3.将对象转化为Json格式格式优化 4.AOP实现Redis缓存 AOP实现Redis @cacheFind()注解 实现策略: A.需要自定义注解C

  • 使用IDEA搭建一个简单的SpringBoot项目超详细过程

    一.创建项目 1.File->new->project: 2.选择"Spring Initializr",点击next:(jdk1.8默认即可) 3.完善项目信息,组名可不做修改,项目名可做修改:最终建的项目名为:test,src->main->java下包名会是:com->example->test:点击next: 4.Web下勾选Spring Web Start,(网上创建springboot项目多是勾选Web选项,而较高版本的Springboo

  • SpringBoot整合Redis实现消息发布与订阅的示例代码

    当我们在多个集群应用中使用到本地缓存时,在数据库数据得到更新后,为保持各个副本当前被修改的数据与数据库数据保持同步,在数据被操作后向其他集群应用发出被更新数据的通知,使其删除;下次当其他应用请求该被更新的数据时,应用会到数据库去取,也就是最新的数据,从而使得被更新数据与数据库保持同步! 能实现发送与接收信息的中间介有很多,比如:RocketMQ.RabbitMQ.ActiveMQ.Kafka等,本次主要简单介绍Redis的推送与订阅功能并集成Spring Boot的实现. 1.添加SpringB

随机推荐