redis 的企业实战应用 (二)

news2025/1/20 13:32:18

前言:

如今redis的常用场景有

  • 短信登录:使用redis共享session来实现

  • 商户查询缓存:会理解缓存击穿,缓存穿透,缓存雪崩等问题,让小伙伴的对于这些概念的理解不仅仅是停留在概念上,更是能在代码中看到对应的内容

  • 优惠卷秒杀:可以学会Redis的计数器功能, 结合Lua完成高性能的redis操作,同时学会Redis分布式锁的原理,包括Redis的三种消息队列

  • 附近的商户:利用Redis的GEOHash来完成对于地理坐标的操作

  • UV统计:主要是使用Redis来完成统计功能

  • 用户签到:使用Redis的BitMap数据统计功能

  • 好友关注:基于Set集合的关注、取消关注,共同关注等等功能,这一块知识咱们之前就讲过,这次我们在项目中来使用一下

  • 打人探店:基于List来完成点赞列表的操作,同时基于SortedSet来完成点赞的排行榜功能

一、短信登录

1.1 导入数据库

数据库版本可根据select version();查询 

 

 导入项目更改成你的端口跟ip,运行报错

报错:NOGROUP No such key ‘stream.orders’ or consumer group ‘g1’ in XREADGROUP with GROUP option

解决方法:创建一个Stream类型的消息队列,名为stream.orders。在redis中执行如下命令:

XGROUP CREATE stream.orders g1 0 MKSTREAM

 重新运行即可

1.2 基于Session实现登录流程

 

 1、发送短信验证码

 @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 session
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

2、短信验证登录注册

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 不一致,报错
            return Result.fail("验证码错误");
        }

        // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        // 5.判断用户是否存在
        if (user == null) {
            // 6.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }

 

 

 

Redis代替session需要考虑的问题: .
◆选择合适的数据结构
◆选择合适的key
◆选择合适的存储粒度

二、商户查询缓存

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.2 添加商户缓存

2.2.1 、缓存模型和思路

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

 代码思路:如果缓存有,则直接返回,如果缓存不存在,则查询数据库,然后存入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 实现商铺和缓存与数据库双写一致

核心思路如下:

修改ShopController中的业务逻辑,满足下面的需求:

根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

根据id修改店铺时,先修改数据库,再删除缓存

修改重点代码1:修改ShopServiceImpl的queryById方法

设置redis缓存时添加过期时间

 

 

修改重点代码2

代码分析:通过之前的淘汰,我们确定了采用删除策略,来解决双写问题,当我们修改了数据之后,然后把缓存中的数据进行删除,查询时发现缓存中没有数据,则会从mysql中加载最新的数据,从而避免数据库和缓存不一致的问题

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

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

常见的解决方案有两种:

  • 缓存空对象

    • 优点:实现简单,维护方便

    • 缺点:

      • 额外的内存消耗

      • 可能造成短期的不一致

  • 布隆过滤

    • 优点:内存占用较少,没有多余key

    • 缺点:

      • 实现复杂

      • 存在误判可能

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

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

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

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

声明: 个人学习记录,不做商业用途,来源于B站黑马程序员redis!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/40968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数学】仿射变换

∣降维打击NightguardSeries.∣\begin{vmatrix}\Huge{\textsf{ 降 维 打 击 }}\\\texttt{ Nightguard Series. }\end{vmatrix}∣∣∣∣∣​ 降 维 打 击 Nightguard Series. ​∣∣∣∣∣​ 注&#xff1a;本文讨论的仿射变换仅为y轴上的伸缩变换&#xff0c;且难度在高中生理…

H3CNE V7.0 视频教程

构建中小企业网络全套PPT汇总【V7版本】 第1章 计算机网络概述 第2章 OSI参考模型与TCP IP模型 第3章 局域网基本原理 第4章 广域网基本原理 第5章 IP基本原理 第6章 TCP和UDP基本原理 第7章 路由器、交换机及其操作系统介绍 第8章 命令行操作基础 第9章 网络设备文件…

mycat-3-实战篇

1 总结&#xff1a; 1&#xff1a;用的表必须在mycat的配置文件中配置。 2&#xff1a;mycat默认分片策略中&#xff0c;都是针对表的主键&#xff0c;默认是id,如果主键不是id的&#xff0c;请去rule.xml自己复制一份修改 3&#xff1a; 2 注意细讲解 1&#xff1a;schem…

Springboot启动流程分析(四):完成启动流程

目录 一 添加BeanPostProcessors到IOC容器 二 国际化支持 三 初始化监听器的多路播放器 四 刷新容器 五 注册监听器到IOC容器的多播器 六 完成bean的大规模实例化 6.1 大规模实例化bean 6.1.1 连续三层do...while循环作用 6.1.2 FactoryBean是什么&#xff1f;为什么要…

04 YAML kubetnetes世界里的通用语

文章目录1. 前言2. 声明式和命令式是怎么回事&#xff1f;3. 什么是YAML&#xff1f;4. 什么是API对象&#xff1f;4.1 k8s都有哪些资源对象4.2 列出kubectl 命令详细执行过程5. 如何描述 API 对象5.1 命令式5.2 声明式5.2.1 声明式YAML语法详解5.2.1.1 header部分详解5.2.1.2 …

【教学类-19-01】20221127《ABAB式-规律排序-A4竖版2份》(中班)

展示效果&#xff1a; 单人使用样式&#xff1a; 单页打印样式 ​ 背景需求&#xff1a; 中班幼儿需要掌握ABAB规律排序&#xff0c;如下图所示&#xff0c;AB两个元素能外形不同、颜色不同。 ​ ​利用Python Word单元格填色功能&#xff0c;随机生成AB样式&#xff0c;引…

STM32模拟IIC与IIC四种实现数字光强采集模块GY30(标准库与HAL库)

目录 代码实现是的IIC通信&#xff0c;数据采集后在串口显示&#xff0c;方便大家实现二次开发 原件选择 GY-30 数字光强度介绍 BH1750芯片参数 引脚说明 BH1750指令集 接线表设计 通过四种方式实现GY-30数据采集 1.标准库模拟IIC实现GY-30采集并串口1显示 2.标准库IIC…

重构uniapp uni-ui coloerUI项目

重构uniapp uni-ui coloerUI项目这里写自定义目录标题重构uniappuni-uicoloerUI项目起源流程重构uniappuni-uicoloerUI项目 起源 从网上复制了若依移动端的代码,但是对里面的文件夹布局方式和第三方组件库引入方式不甚了解,就想着从头创建一个空白项目&#xff0c;然后一步一…

Linux中设置开机启动执行命令和普通用户配置环境变量开机启动生效

记录&#xff1a;343 场景&#xff1a;在CentOS 7.9操作系统上&#xff0c;开机启动就执行自定义的命令&#xff0c;配置rc.local文件达到需求&#xff1b;在普通用户中配置环境变量开机启动生效&#xff0c;使用profile实现。 版本&#xff1a; 操作系统&#xff1a;CentOS…

01、Docker入门

目录 1、Docker是什么 2、Docker与虚拟化 3、Docker虚拟化的好处 好处一&#xff1a;应用部署方便 好处二&#xff1a;服务器同等配置&#xff0c;性能更优&#xff0c;利用率更高 4、核心概念 5、CentOS7 安装docker(在线方式) 6、镜像 7、Docker容器 8、查看Docker容…

typescript 八叉树的简单实现

查了一些文章&#xff0c;准备自己用typescript写一个简单的八叉树场景管理。 所谓的简单&#xff0c;就是所有元素都是边长为1的立方体。 元素类和树节点类 //元素类&#xff0c;因为都是边长为1的立方体&#xff0c;所以就用cube命名 export class CubeData {public reado…

由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开

一、问题描述 在使用Windows的远程桌面工具连接WindowsServer2016服务器时&#xff0c;无法连接到服务器&#xff0c;并且提示【由于没有远程桌面授权服务器可以提供许可证&#xff0c;远程回来连接已经断开。请跟服务器管理员联系】。 二、解决办法 2.0、前提 Windows Serv…

黑胶歌曲没权限,看我python大展神通,一分钟一个歌单

前言 大家早好、午好、晚好吖 ❤ ~ 人之初&#xff0c;喜白嫖。 大家都喜欢白嫖&#xff0c;我也喜欢&#xff0c;那么今天就来试试怎么白嫖抑云~ 一、需要的准备 1、环境 Python3.6以上 pycharm2019以上 2、模块 requests # 发送请求模块 第三方模块 exec js # 调用JS的…

CocosCreater 教程(下)

1.物理系统 1.1 2D刚体 刚体是组成物理世界的基本对象。 1.2 2D 碰撞组件 目前引擎支持三种不同的碰撞组件&#xff1a; 盒碰撞组件&#xff08;BoxCollider2D&#xff09;、圆形碰撞组件&#xff08;CircleCollider2D&#xff09; 和 多边形碰撞组件&#xff08;PolygonCo…

Java中的抽象类和接口

java中的抽象类和接口抽象类什么是抽象类&#xff1f;抽象的使用场景抽象类的案例抽象类的特征、注意事项小结抽象类的应用知识&#xff1a;模版方法模式接口接口概述、特点接口的基本使用&#xff1a;被实现接口与接口的关系&#xff1a;多继承JDK8开始接口新增方法接口的注意…

AtCoder Beginner Contest 277 F. Sorting a Matrix(拓扑排序+虚点)

题目 n*m(2<n,m<1e6,n*m<1e6)的矩阵&#xff0c; 第i行第j列元素a[i][j](0<a[i][j]<n*m) 对于值为0的元素&#xff0c;你可以将其赋值为任意正整数&#xff0c; 不同位置的0元素&#xff0c;可以被赋值成不同的正整数 然后&#xff0c;你可以执行以下操作若…

firefly3399 移植linux5.15.80 - 2022-11-27

需要注意的是&#xff0c;虚拟机需要足够的硬盘空间&#xff0c;不小于15GB&#xff01;&#xff01; 一、内核源码下载 国内镜像地址 git clone https://kernel.source.codeaurora.cn/pub/scm/linux/kernel/git/stable/linux.git/ 基本达到了带宽的最大值。 国外地址&#…

Android使用AudioTrack播放WAV音频文件

目录 1、wav文件格式 2、wav文件解析 3、wav文件播放 QA&#xff1a; 开始播放wav的时候使用了系统的播放器mediaplayer进行播放&#xff0c;但是无奈mediaplayer支持的实在不好。 好些年前自己做过pcm播放使用的是audiotrack&#xff0c;参考&#xff1a;CSDN 其实两者之…

php 进程池设计与实现,phper必学!

php 进程池设计与实现phper 为什么要学习进程池池的概念为什么要有进程池?动态创建进程缺点进程池的优点选择子进程为新任务服务的方式进程池模型服务端客户端结语phper 为什么要学习进程池 在php开发过程中经常使用的 php-fpm 使用的进程模型就是进程池&#xff0c;学习进程…

如何基于FSM有限状态机实现Enemies AI

文章目录&#x1f35f; Preface&#x1f355; 巡逻状态&#x1f37f; 寻路状态&#x1f32d; 攻击状态&#x1f357; 完整代码&#x1f35f; Preface 本文简单介绍如何基于FSM有限状态机实现Enemies AI&#xff0c;首先定义敌人的AI逻辑&#xff1a;默认状态下Enemy为巡逻状态…