基于社区电商的Redis缓存架构-缓存数据库双写、高并发场景下优化
基于社区电商的Redis缓存架构
首先来讲一下 Feed 流的含义:
Feed 流指的是当我们进入 APP 之后,APP 要做一个 Feed 行为,即主动的在 APP 内提供各种各样的内容给我们
在电商 APP 首页,不停在首页向下拉,那么每次拉的时候,APP 就会根据你的喜好、算法来不停地展示新的内容给你看,这就是电商 APP 的 Feed 流了
那么接下来呢就基于首页 Feed 流以及社区电商 APP 中的一些业务场景,来实现一套基于 Redis 的企业级缓存架构,MySQL 为基础,RocketMQ 为辅助
那么在缓存中常见的问题有:
- 热 key 问题
- 大 key 问题
- 缓存雪崩(穿透)
- 数据库和缓存数据一致性问题
在 Redis 生产环境中,也存在一些问题,如下:
- Redis 集群部署,需要进行高并发压测
- 监控 Redis 集群:每个节点数据存储情况、接口 QPS、机器负载情况、缓存命中率
- Redis 节点故障的主从切换、Redis 集群扩容
下边我们来逐个剖析在缓存架构中常见的一些问题
首先,热 key 举个例子就是微博突然某个明星出现新闻,那么会有大量请求去访问这个数据,这个 key 就是热 key
大 key 指的是某个 key 所存储的 value 很大,value 多大 10 mb,那么如果读取这个大 key 过于频繁,就会对网络带宽造成影响,阻塞其他请求
缓存雪崩是因为大量缓存数据同时过期或者 Redis 集群故障,如果因为缓存雪崩导致 Redis 集群都崩掉了,那么此时只有数据库可以访问,我们的系统需要可以识别出来缓存故障,立马对各个接口进行限流、降级错误,来保护数据库,避免数据库崩掉,可以在 jvm 内存缓存中存储少量缓存数据,用于在 Redis 崩了之后提供降级备用数据,那么流程为:限流 --> 降级 --> jvm 缓存数据
读多写少数据缓存
那么我们先来分析一下在社区电商中,用户去分享一个内容时,如何操作缓存:
- 用户分享内容
- 加锁:针对用户 id 上分布式锁,避免同一用户重复请求,导致数据重复灌入
- 将用户分享内容写入数据库
- 将用户的个人信息在缓存写一份
(用户信息在注册后一般不会变化,读多写少,因此用户信息非常适合放入缓存中)
,这样在后续高并发访问用户的数据时,就可以在缓存中进行查询,根据用户前缀 + 用户 id 作为 key,并设置缓存过期时间,过期时间可以设置为 2 天加上随机几小时(添加随机几小时的原因是避免同一时间大量缓存同时过期) - 释放锁
缓存自动延期以及缓存穿透
那么上边我们已经对用户的个人信息进行了缓存,那么某些热门的用户是经常被很多人看到的,而有些冷门用户的内容没多少人看,因此将用户信息的缓存过期时间设置为 2天+随机几小时
,因此我们针对热门的用户信息数据,要做一个缓存自动延期,因此只要访问用户数据,那么就对该缓存数据进行一个延期,流程如下:
- 获取用户个人信息
- 根据
用户前缀 + 用户id
作为 key 去缓存中查询用户信息
public UserInfo getUserInfo(Integer userId) {// 读缓存String userInfoJson = redisCache.get("user_info_lock:" + userId);if (!StringUtils.isEmpty(userInfoJson)) {// 取到的是空缓存 避免一致访问数据库不存在的数据导致缓存穿透if ("{}".equals(userInfoJson)) {// 延期缓存redisCache.expire("user_info_lock:" + userId, expireSecond);return null;} else {// 延期缓存redisCache.expire("user_info_lock:" + userId, expireSecond);UserInfo userInfo = JSON.parseObject(productStr, UserInfo.class);}}// 如果缓存中没有取到数据,则在数据库中查,并放入缓存lock("user_lock_prefix" + userId); // 上分布式锁 伪代码try {// 读数据库UserInfo userInfo = userInfoService.get(userId);if(userInfo != null) {// 如果用户信息不为空,就将用户数据写入缓存redisCache.set("user_info_lock:" + userId, JSON.toJSONString(userInfo), expireSecond);} else {// 如果数据库为空,写入一个空字符串即可redisCache.set("user_info_lock:" + userId, "{}", expireSecond);}} finally {unlock("user_lock_lock:" + userId); // 解锁}
}
缓存+数据库双写不一致
在上边存储用户个人信息时,使用的是 缓存+数据库双写
,这样可能造成数据不一致性,如下:
有两个线程并发,一个读线程,一个写线程,假设执行流程如下,会造成双写不一致
- 当读线程去缓存中读取数据,此时缓存中数据正好过期,那么该线程就去读数据库中的数据
- 此时写线程开始执行,修改用户信息,并且写入数据库,再写入缓存
- 此时读线程再接着执行,将之前在数据库中读取的旧数据写入缓存,覆盖了写线程更新后的数据
造成这种情况的原因是,在读的时候,加的是读锁,key 为 user_info_lock:
,在写的时候,加的是写锁,key 为 user_update_lock:
,那么读写就可以并发
想要解决的话,可以让读和写操作加同一把锁,让读写串行化,就可以了,如下:
让读和写都加同一把锁:user_update_lock
- 针对先读后写的情况,就不会出现双写不一致了,读的时候先加上锁
user_update_lock
,此时缓存假设正好过期,去数据库中读取数据,此时写线程开始执行,阻塞等待锁user_update_lock
,此时读线程再去数据库读取数据放入缓存,结束后释放锁,那么写线程再拿到锁操作数据库,再将数据写入缓存,那么缓存中的数据是新数据 - 针对先写后读的情况,也不会出现双写不一致,在写的时候,加上锁
user_update_lock
,那么在并发读的时候,先获取缓存内容,如果获取不到,尝试去 DB 中获取,此时就会阻塞等待锁user_update_lock
,在写线程写完之后更新了缓存,释放锁,此时读线程就拿到了锁,此时在去数据库中查数据之前再加一个double check(双端检锁)
的操作,也就是再尝试去缓存中取一次数据,如果取到了就返回;如果没有取到,就去数据库中查询
那么完整的写和读操作流程如下图:
高并发场景下优化
在上边我们已经使用读写加同一把锁来实现缓存数据库双写一致了
但是还存在一种极端情况:某一个用户的信息并不在缓存中,但是突然火了,大量用户来访问,发现缓存中没有,那么大量用户线程就阻塞在了获取锁的这一步操作上,导致大量线程串行化的来获取锁,然后再到缓存中获取数据,下一个线程再获取锁取数据
这种情况的解决方案就是给获取锁加一个超时时间,如果在 200ms 内没有拿到锁,就算获取锁失败,这样大量用户线程获取锁失败,就会从串行再转为并发从缓存中取数据了,避免大量线程阻塞获取锁
完整流程图如下,粉色部分为优化:
代码如下:
private UserInfo getUserInfo(Long userId) {String userLockKey = "user_update_lock:" + userId;boolean lock = false;try {// 尝试加锁,并设置超时时间为 200 mslock = redisLock.tryLock("user_update_lock:", 200);} catch(InterruptedException e) {UserInfo user = getFromCache(userId);if(user != null) {return user;}log.error(e.getMessage(), e);throw new BaseBizException("查询失败");}// 如果加锁超时,就再次去缓存中查询if (!lock) {UserInfo user = getFromCache(userId);if(user != null) {return user;}// 缓存数据为空,查询用户信息失败,因为用户没有拿到锁,因此也无法取 DB 中查询throw new BaseBizException("查询失败");}// 双端检锁,如果拿到锁,再去缓存中查询try {UserInfo user = getFromCache(userId);if(user != null) {return user;}String userInfoKey = "user_info:" + userId;// 数据库中查询user = userService.getById(userId);if (Objects.isNull(user)) {redisCache.set(userInfoKey, "{}", RandomUtil.genRandomInt(30, 100));return null;}// 缓存时间设置为 2 天 + 随机几小时redisCache.set(userInfoKey, JSON.toJSONString(user), 2 * 24 * 60 * 60RandomUtil.genRandomInt(0, 10) * 60 * 60);return user;} finally {redisLock.unlock(userLockKey);}
相关文章:

基于社区电商的Redis缓存架构-缓存数据库双写、高并发场景下优化
基于社区电商的Redis缓存架构 首先来讲一下 Feed 流的含义: Feed 流指的是当我们进入 APP 之后,APP 要做一个 Feed 行为,即主动的在 APP 内提供各种各样的内容给我们 在电商 APP 首页,不停在首页向下拉,那么每次拉的…...

Python提取PDF表格(基于AUTOSAR_SWS_CANDriver.pdf)
个人学习笔记,仅供参考。 需求:提取AUTOSAR SWS中所有的API接口信息,用于生成C代码。 此处以AUTOSAR_SWS_CANDriver.pdf为例,若需要提取多个SWS文件,遍历各个文件即可。 1.Python包 pdfplumber是一款完全用python开…...
UVa1583生成元(Digit Generator)
题目 如果x加上x的各个数字之和得到y,也就是说x是y的生成元。给出n(1<n<100000),求最小生成元。无解则输出0。 输入输出样例 输入 3 216 121 2005输出 198 0 1979思路 要想解决这个题目,只需要对每一个输入的值从1开始遍历找到小于…...

【Springboot+vue】如何运行springboot+vue项目
从github 或者 gitee 下载源码后,解压,再从idea打开项目 后端代码处理 这是我在gitee下载下来的源码 打开之后,先处理后端代码 该配置的配置,该部署的部署 比如将sql文件导入数据库 然后去配置文件更改配置 然后启动项目 确保…...

拥抱变化,良心AI工具推荐
文章目录 💥 简介🍄 工具介绍🍓 功能特点🥗 使用场景🎉 用户体验🧩 下载地址🍭 总结 💥 简介 我是一名资深程序员,但薪资缺对不起资深两个字,为了生存&#x…...

Tensorflow的日志log记录
if OUTPUT_GRAPH:tf.summary.FileWriter("logs/", sess.graph)自动创建文件夹log...

C-语言每日刷题
目录 [蓝桥杯 2015 省 A] 饮料换购 题目描述 输入格式 输出格式 输入输出样例 # [蓝桥杯 2023 省 A] 平方差 题目描述 输入格式 输出格式 输入输出样例 说明/提示 【样例说明】 [NOIP2001 普及组] 数的计算 题目描述 输入格式 输出格式 输入输出样例 说明/提示 样例 1 解释 数据…...

十五届海峡两岸电视主持新秀大会竞赛流程
海峡两岸电视主持新秀会是两岸电视媒体共同举办的一项活动,旨在为两岸年轻的电视主持人提供一个展示才华的舞台,促进两岸文化交流和青年交流。本届新秀会是第十二届海峡两岸电视艺术节的重要活动之一。本次竞赛赛制流程如下: (1&…...
安全行业招聘信息汇总
1. 阿里巴巴-淘天集团-安全部 社招岗位:Java开发 招聘层级:P5-P6 工作年限:本科毕业1-3年,硕士毕业1-2年 base地点:杭州 职位描述 负责淘天安全部风控基础标签平台0到1能力建设及产品规划和落地。负责标签应用的产品…...

【如何学习python自动化测试】—— 浏览器驱动的安装 以及 如何更新driver
之前讲到基于python的自动化测试环境,需要安装Python,再安装Selenium。具体可看【如何学习Python自动化测试】—— 自动化测试环境搭建 但是,想要使用Selenium发送指令模拟人类行为操作浏览器,就需要安装浏览器驱动。不同的浏览器需要安…...

Spring Data Redis切换底层Jedis 和 Lettuce实现
1 简介 Spring Data Redis是 Spring Data 系列的一部分,它提供了Spring应用程序对Redis的轻松配置和使用。它不仅提供了对Redis操作的高级抽象,还支持Jedis和Lettuce两种连接方式。 可通过简单的配置就能连接Redis,并且可以切换Jedis和Lett…...

wireshark自定义协议插件开发
目录 脚本代码 报文显示 脚本代码 local NAME "test" test_proto Proto("test", "test Protocol") task_id ProtoField.uint16("test.task_id", "test id", base.DEC) cn ProtoField.uint8("test.cn", &qu…...

一文读懂MongoDB的全部知识点(1),惊呆面试官。
文章目录 01、mongodb是什么?02、mongodb有哪些特点?03、你说的NoSQL数据库是什么意思?NoSQL与RDBMS直接有什么区别?为什么要使用和不使用NoSQL数据库?说一说NoSQL数据库的几个优点?04、NoSQL数据库有哪些类型?05、M…...

仅仅通过提示词,GPT-4可以被引导成为多个领域的特定专家
The Power of Prompting:提示的力量,仅通过提示,GPT-4可以被引导成为多个领域的特定专家。微软研究院发布了一项研究,展示了在仅使用提策略的情况下让GPT 4在医学基准测试中表现得像一个专家。研究显示,GPT-4在相同的基…...
23.Oracle11g的UNDO表空间
Oracle的UNDO表空间 一、UNDO表空间概述1、什么是UNDO表空间2、UNDO表空间的作用2.1 提供一致性读2.2 回滚事务2.3 实例恢复 3、UNDO表空间的工作机制 二、UNDO表空间的相关操作1、UNDO表空间的创建2、UNDO表空间的管理 三、Oracle 11g中UNDO表空间的新特性1、UNDO表空间自动管…...

Mybatis 操作续集2(结合上文)
Mybatis 是一个持久层框架,用于简化数据库的操作,和Spring 没有任何关系,我们现在能使用它是因为 Spring Boot 把Mybatis 的依赖给引入进来了,在 pom.xml 里面 Mybatis 如何进行重命名? 看最后两行代码,这样就能重命名了 package com.example.mybatisdemo.mapper;import com…...

LangChain 19 Agents Reason+Action自定义agent处理OpenAI的计算缺陷
LangChain系列文章 LangChain 实现给动物取名字,LangChain 2模块化prompt template并用streamlit生成网站 实现给动物取名字LangChain 3使用Agent访问Wikipedia和llm-math计算狗的平均年龄LangChain 4用向量数据库Faiss存储,读取YouTube的视频文本搜索I…...
12.整数转罗马数字
C不能像Python那样使用c*num的形式,重复字符,老老实实减吧。 class Solution { public:string intToRoman(int num) {string ans "";int res num;while (res ! 0) {if (res > 1000) {ans M;res - 1000;} else if (res > 900) {ans …...

免费AI洗稿软件【2023最新】
很多时候我们需要通过文字来表达观点、推广产品或服务。然而,长时间的文稿创作不仅费时费力,还容易陷入表达瓶颈。许多写手和从业者纷纷寻找一款方便、高效的AI洗稿工具。 文心一言洗稿软件。这款软件以其独特的文风生成和洗稿功能而备受瞩目。用户只需…...
PTA:平方回文数
题干 在数学里面,有一种数字正着读和反着读结果都一样,我们把这种数称为回文数。如果一个回文数,它同时还是某一个数的平方,这样的数字叫做平方回数。 本题要求输出小于正整数N的所有平方回数。 (注:个位数…...

Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...

高等数学(下)题型笔记(八)空间解析几何与向量代数
目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...

零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
本期内容并不是很难,相信大家会学的很愉快,当然对于有后端基础的朋友来说,本期内容更加容易了解,当然没有基础的也别担心,本期内容会详细解释有关内容 本期用到的软件:yakit(因为经过之前好多期…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...
PostgreSQL——环境搭建
一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在࿰…...