缓存和分布式锁笔记
缓存
开发中,凡是放入缓存中的数据都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题。
redis作为缓存使用redisTemplate操作redis
分布式锁的原理和使用
分布式加锁:本地锁,只能锁住当前进程,所以我们需要分布式锁
分布式锁演进
基本原理:多个操作用户操作,抢占锁,获取到锁的用户执行业务,释放锁。
分布式锁演进阶段1:
redis获取锁:setnx(“lock”,1111) -->获取到锁->执行业务->删除锁->结束,未获取到锁的等待重试
代码:
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {//1. 分布式锁 去redis占坑Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");if(lock){//加锁成功Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();redisTemplate.delete("lock");return dataFromDb;}else {//加锁失败 重试 synchronizereturn getCatalogJsonFromDbWithRedisLock();//自旋的方式}
}
问题:
setnx占好了位,业务代码异常或者程序在页面过程中宕机,没有执行删除锁逻辑,造成死锁
解决:
设置锁的自动过期,即使没有删除,会自动删除
分布式锁演进阶段2:
redis获取锁:setnx(“lock”,1111) -->获取到锁->设置过期时间->执行业务->删除锁->结束,未获取到锁的等待重试
代码:
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {//1. 分布式锁 去redis占坑Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");if(lock){//加锁成功//2. 设置过期时间redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();redisTemplate.delete("lock");return dataFromDb;}else {//加锁失败 重试 synchronizereturn getCatalogJsonFromDbWithRedisLock();//自旋的方式}
}
问题:
setnx设置好,正要去设置过期时间,宕机,死锁。
解决:
设置过期时间和占位必须是原子的,redis支持使用setnx ex命令
分布式锁演进阶段3:
redis获取锁:setnxex(“lock”,1111,10s) -->获取到锁->执行业务->删除锁->结束,未获取到锁的等待重试
代码:
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {//1. 分布式锁 去redis占坑Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);if(lock){//加锁成功//2. 设置过期时间// redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();redisTemplate.delete("lock");return dataFromDb;}else {//加锁失败 重试 synchronizereturn getCatalogJsonFromDbWithRedisLock();//自旋的方式}}
问题:
删除锁直接删除?由于业务时间很长,锁自己过期了,直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为uuid,每个人匹配的是自己的锁才删除
分布式锁演进阶段4:
redis获取锁:setnxex(“lock”,uuid,10s) -->获取到锁->执行业务->如果当前锁的值是之前的uuid的锁–>删除锁->结束,未获取到锁的等待重试
代码:
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {String uuid = UuidUtils.generateUuid().toString();//1. 分布式锁 去redis占坑Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);if(lock){//加锁成功//2. 设置过期时间// redisTemplate.expire("lock",30,TimeUnit.SECONDS);Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();String lockValue = redisTemplate.opsForValue().get("lock");if(lockValue.equals(uuid)) {//删除自己的锁redisTemplate.delete("lock");}return dataFromDb;}else {//加锁失败 重试 synchronizereturn getCatalogJsonFromDbWithRedisLock();//自旋的方式}}
问题:
如果正好判断当前值,正要删除锁的时候,锁已经过期别人已经设置到了新的值,删除的还是别人的锁
解决:
删除锁必须保证原子性,使用redis+lua脚本
分布式锁演进阶段5:
redis获取锁:setnxex(“lock”,uuid,10s) -->获取到锁->执行业务->脚本解锁保证原子性->结束,未获取到锁的等待重试
代码:
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {String uuid = UuidUtils.generateUuid().toString();//1. 分布式锁 去redis占坑Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);if(lock){//加锁成功Map<String, List<Catelog2Vo>> dataFromDb = null;try {dataFromDb = getDataFromDb();}finally {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";//删除锁Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);}// String lockValue = redisTemplate.opsForValue().get("lock");// if(lockValue.equals(uuid)) {// //删除自己的锁// redisTemplate.delete("lock");// }return dataFromDb;}else {//加锁失败 重试 synchronizereturn getCatalogJsonFromDbWithRedisLock();//自旋的方式}}String script = "if redis.call"('get',KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期
锁
可重入锁(Reentrant Lock)
某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,再次获取锁的时候会判断当前线程是否是已经加锁的线程,如果是对锁的次数+1,释放锁的时候加了几次锁,就需要释放几次锁。
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
@ResponseBody@GetMapping("/hello")public String hello(){//1.获取一把锁,只要锁的名字一样,就是同一把锁RLock lock = redisson.getLock("my-lock");//2.加锁lock.lock();//阻塞式等待 默认加的锁是30s//1. 锁的自动续期 如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期会删除//2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除try {System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());Thread.sleep(30000);}catch (Exception e){e.printStackTrace();}finally {//3.解锁System.out.println("释放锁"+Thread.currentThread().getId());lock.unlock();}return "hello";}
问题:负责存储分布式锁的Redission节点宕机后,这个锁正好处于锁住的状态时,这个锁会出现锁死的状态
解决:reddison内部提供了一个监控锁的看门狗,作用是在redission实例被关闭前,不断的延长锁的有效期,默认情况下,看门狗的检查锁的超时时间是30秒钟,可以通过Config.lockWatchdogTimeout。还通过加锁的方法提供了leaseTime的参数来指定加锁的时间,超过时间后锁便自动解开了。
读写锁
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
@GetMapping("/write")@ResponseBodypublic String writeValue(){String s="";RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");RLock lock = readWriteLock.writeLock();try {//改数据加写锁 读数据加读锁lock.lock();s = UUID.randomUUID().toString();Thread.sleep(30000);redisTemplate.opsForValue().set("writeValue",s);}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}return s;}@GetMapping("/read")@ResponseBodypublic String readValue(){String s="";RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");//加读锁Lock rLock = writeLock.readLock();try {rLock.lock();s = redisTemplate.opsForValue().get("writeValue").toString();Thread.sleep(30000);}catch (Exception e){e.printStackTrace();}finally {rLock.unlock();}return s;}
结论:
- 保证一定可以读到最新的数据,修改期间,写锁是一个排他锁(互斥锁,独享锁).读锁是一个共享锁
- 写锁没有释放 读就必须等待
- 读 + 读:相当于无锁并发读,只会的redis中记录好,所有当前的读锁,他们都会同时加锁成功
- 写 + 读:等待写锁释放
- 写 + 写:阻塞方式
- 读 + 写:有读锁也需要等待
- 只要有写锁的存在,都必须等待
信号量
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
这里以停车位为例,当停车时,获取一个信号量,获取到信号量之后进行停车,车开走之后可以再释放一个信号量
/*** 车库停车* @return* @throws InterruptedException* 信号量 可以用作分布式限流*/
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {RSemaphore park = redisson.getSemaphore("park");park.acquire();//获取一个信号量,获取一个信号量占一个车位return "ok";
}@GetMapping("/go")
@ResponseBody
public String go(){RSemaphore park = redisson.getSemaphore("park");park.release();//释放一个车位return "ok";
}
闭锁
原理:闭锁相当于一扇门,在闭锁到达结束状态之前,这扇门一直是关闭着的,没有任何线程可以通过,当到达结束状态时,这扇门才会打开并容许所有线程通过。它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,初始化为一个正式,正数表示需要等待的事件数量。countDown方法递减计数器,表示一个事件已经发生,而await方法等待计数器到达0,表示等待的事件已经发生。CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行。
应用场景
10个运动员准备赛跑,他们等待裁判一声令下就开始同时跑,当最后一个人通过终点的时候,比赛结束。10个运动相当于10个线程,这里关键是控制10个线程同时跑起来,还有怎么判断最后一个线程到达终点。可以用2个闭锁,第一个闭锁用来控制10个线程等待裁判的命令,第二个闭锁控制比赛结束。
示例
5个班放学,当5个班的同学都走完之后,锁门
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {RCountDownLatch door = redisson.getCountDownLatch("door");door.trySetCount(5);door.await();return "放假了";
}@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id){RCountDownLatch door = redisson.getCountDownLatch("door");door.countDown();//计算减一return id+"班的人走完了";
}
数据一致性问题
- 双写模式
- 失效模式
- 解决方案
- 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。
- 如果是用户维度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
- 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);
- 总结:
- 放入缓存的数据本不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 不应该过度设计,增加系统的复杂性 • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
Spring Cache
- Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等
- 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。
- 使用 Spring 缓存抽象时我们需要关注以下两点;
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
参考:缓存和分布式锁
相关文章:
缓存和分布式锁笔记
缓存 开发中,凡是放入缓存中的数据都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题。 redis作为缓存使用redisTemplate操作redis 分布式锁的原理和使用 分布式加锁&…...
React笔记(七)Antd
一、登录功能 首先要使用antd,要先下载 yarn add antd 登录页面关键代码 import React from react /*1、如果要在react中完成样式隔离,需要如下操作1)命名一个xx.module.scss webpack要求2) 在需要的组件中通过ES6方式进行导入&#x…...

无涯教程-Android - RadioButton函数
RadioButton有两种状态:选中或未选中,这允许用户从一组中选择一个选项。 Radio Button 示例 本示例将带您完成一些简单的步骤,以展示如何使用Linear Layout和RadioButton创建自己的Android应用程序。 以下是修改后的主要Activity文件 src/MainActivity.java 的内容。 packa…...
kafka如何避免消费组重平衡
目录 前言: 协调者 重平衡的影响 避免重平衡 重平衡发生的场景 参考资料 前言: Rebalance 就是让一个 Consumer Group 下所有的 Consumer 实例就如何消费订阅主题的所有分区达成共识的过程。在 Rebalance 过程中,所有 Consumer 实例…...
浅谈一下企业信息化管理
企业信息化管理 企业信息化是指将企业的生产过程,物料,事务,财务,销售等业务过程数字化,通过各种信息系统网络价格成新的信息资源,提供给各层次的人们东西观察各类动态业务中的一切信息,以便于…...

北京APP外包开发团队人员构成
下面是一个标准的APP开发团队构成,但具体的人员规模和角色可能会根据项目的规模和需求进行调整。例如,一些小型项目或初创公司可能将一些角色合并,或者聘请外包团队来完成部分工作。北京木奇移动技术有限公司,专业的软件外包开发公…...

Node基础and包管理工具
Node基础 fs 模块 fs 全称为 file system,称之为 文件系统,是 Node.js 中的 内置模块,可以对计算机中的磁盘进行操作。 本章节会介绍如下几个操作: 1. 文件写入 2. 文件读取 3. 文件移动与重命名 4. 文件删除 5. 文件夹操作 6. …...

【python使用 Pillow 库】缩小|放大图片
当我们处理图像时,有时候需要调整图像的大小以适应特定的需求。本文将介绍如何使用 Python 的 PIL 库(Pillow)来调整图像的大小,并保存调整后的图像。 环境准备 在开始之前,我们需要安装 Pillow 库。可以使用以下命令…...

解决Ubuntu 或Debian apt-get IPv6问题:如何设置仅使用IPv4
文章目录 解决Ubuntu 或Debian apt-get IPv6问题:如何设置仅使用IPv4 解决Ubuntu 或Debian apt-get IPv6问题:如何设置仅使用IPv4 背景: 在Ubuntu 22.04(包括 20.04 18.04 等版本) 或 Debian (10、11、12)系统中,当你使用apt up…...

Xubuntu16.04系统中解决无法识别exFAT格式的U盘
问题描述 将exFAT格式的U盘插入到Xubuntu16.04系统中,发现系统可以识别到此U盘,但是打不开,查询后发现需要安装exfat-utils库才行。 解决方案: 1.设备有网络的情况下 apt-get install exfat-utils直接安装exfat-utils库即可 2.设备…...

Pygame中Trivia游戏解析6-1
1 Trivia游戏简介 Trivia的含义是“智力测验比赛中的各种知识”。Trivia游戏类似智力竞赛,由电脑出题,玩家进行作答,之后电脑对玩家的答案进行判断,给出结果并进行评分。该游戏的界面如图1所示。 图1 Trivia游戏界面 2 游戏流程 …...

idea中创建springboot项目显示Spring Initializr Error
很长时间不创建springboot项目了,今天发现创建完成idea显示: Spring Initializr Error error:status:500项目中没有pom.xml文件.检查了一下原因是在创建的时候类型没有创建正确(之前记得都是默认),默认如下 需要选择创建maven完整工程那种,最下面那种只会生成pom.xml不会…...

VScode 国内下载源 以及 nvm版本控制器下载与使用
VScode 国内下载源 进入官网 https://code.visualstudio.com/ 点击下载 复制下载链接到新的浏览器标签 将地址中的/stable前的az764295.vo.msecnd.net换成vscode.cdn.azure.cn,再回车就会直接在下载列表啦。 参考大神博客 2.使用nvm 对 node 和npm进行版本控制…...
GO|经典错误之回车与\n
学习go的输入输出语句,于是在笔记本上写了这么一段代码: func main() {reader : bufio.NewReader(os.Stdin)input, _ : reader.ReadString(\n)input input[:len(input)-1]i, _: strconv.Atoi(input)fmt.Println(i) } 运行,输入99ÿ…...

【MATLAB第71期】基于MATLAB的Abcboost自适应决策树多输入单输出回归预测及多分类预测模型(更新中)
【MATLAB第71期】基于MATLAB的Abcboost自适应决策树多输入单输出回归预测及多分类预测模型(更新中) 一、效果展示(多分类预测) 二、效果展示(回归预测) 三、代码获取 CSDN后台私信回复“71期”即可获取下…...

ARM编程模型-内存空间和数据
ARM属于RISC体系,许多指令单周期指令,是32位读取/存储架构,对内存访问是32位,Load and store的架构,只有寄存器对内存,不能内存对内存存储,CPU通过寄存器对内存进行读写操作。 ARM的寻址空间是线…...
leetcode原题: 最大数
题目: 给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。 注意:输出结果可能非常大 所以你需要返回一个字符串而不是整数。 示例1: 输入:nums [10,2] 输…...
docker 是什么
目录 docker是一个软件 Docker 是一种运行于 Linux 和 Windows 上的软件,用于创建、管理和编排容器。 为什么要使用 Docker? 1、 更快速的交付和部署 2、 更高效的虚拟化 3、 更轻松的迁移和扩展 4 、更简单的管理 docker是一个软件,是…...

基于Gin框架的HTTP接口限速实践
在当今的微服务架构和RESTful API主导的时代,HTTP接口在各个业务模块之间扮演着重要的角色。随着业务规模的不断扩大,接口的访问频率和负载也随之增加。为了确保系统的稳定性和性能,接口限速成了一个重要的话题。 1 接口限速的使用场景 接口…...

WSL中为Ubuntu和Debian设置固定IP的终极指南
文章目录 **WSL中为Ubuntu和Debian设置固定IP的终极指南****引言/背景****1. 传统方法****2. 新方法:添加指定IP而不是更改IP****结论**WSL中为Ubuntu和Debian设置固定IP的终极指南 引言/背景 随着WSL(Windows Subsystem for Linux)的普及,越来越多的开发者开始在Windows…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器
拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件: 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…...