缓存和分布式锁笔记
缓存
开发中,凡是放入缓存中的数据都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题。
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…...
vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
JS手写代码篇----使用Promise封装AJAX请求
15、使用Promise封装AJAX请求 promise就有reject和resolve了,就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...
