Springboot高并发乐观锁

Spring Boot分布式锁的主要缺点包括但不限于以下几点:

  1. 性能开销:使用分布式锁通常涉及到网络通信,这会引入额外的延迟和性能开销。例如,当使用Redis或Zookeeper实现分布式锁时,每次获取或释放锁都需要与这些服务进行交互。

  2. 单点故障风险:如果依赖于某个特定的服务(如Redis)来管理锁,那么该服务可能会成为单点故障。如果这个服务不可用,所有依赖它的锁机制都会失效,可能导致系统不稳定或者数据不一致的问题。

  3. 死锁风险:在某些情况下,如果没有正确处理异常情况或者客户端突然崩溃,可能会导致死锁现象。例如,如果一个持有锁的进程未能正确释放锁,则其他等待该锁的进程将永远处于等待状态。

  4. 复杂性增加:引入分布式锁增加了系统的复杂性。开发人员需要理解如何正确地使用锁,并且要考虑到各种边界条件,比如超时、重试逻辑等。此外,还需要考虑不同类型的锁(如公平锁、非公平锁)以及它们对应用行为的影响。

  5. 资源竞争:在高并发场景下,多个实例尝试同时获取同一把锁会导致大量的资源竞争,从而影响整体性能。特别是对于一些频繁读写的热点数据来说,这种竞争可能会成为一个瓶颈。

  6. 实现差异:不同的分布式锁实现之间存在差异,这意味着迁移到另一种解决方案可能需要更改代码甚至重新设计架构。而且,不是所有的实现都提供了相同的特性和保障。

  7. 租约管理和心跳检测:一些分布式锁实现依赖于租约(Lease)和心跳来确保锁的有效性。这要求客户端定期向锁服务发送心跳信号以保持其持有的锁。如果网络分区发生或客户端出现故障,可能会导致锁提前被释放,进而引发数据一致性问题。

  8. 不适合长时间持有锁:由于网络延迟和其他因素,长时间持有分布式锁不是一个好的实践,因为它可能会阻塞其他请求过久,尤其是在高并发环境中。

Redis与Lua

使用Redis与Lua脚本结合的方式虽然有很多优点,比如减少网络开销、提供原子性操作以及可复用等特性,但也存在一些缺点:

  1. 脚本大小和执行时间限制

    • Lua脚本的大小受到一定的限制,过大的脚本可能无法成功加载到Redis中。
    • Redis对Lua脚本的执行时间也有一定限制,以防止单个脚本占用过多资源或导致服务器阻塞。如果脚本执行时间过长,可能会触发客户端配置的时间限制,进而中断脚本执行。
  2. 编写复杂度

    • 编写Lua脚本需要一定的编程经验,对于不熟悉Lua语言或者编程概念的开发者来说,可能存在较高的学习曲线。
    • 如果Lua脚本逻辑复杂,调试和维护也会变得更加困难。
  3. 阻塞风险

    • 在Redis中,Lua脚本是按照顺序串行执行的,并且在执行期间会阻止其他命令的处理。因此,长时间运行的脚本可能会造成Redis服务器的阻塞,影响系统的响应速度和其他客户端的操作。
    • 不应该在Lua脚本中使用阻塞命令(如BLPOP, BRPOP等),因为这会导致Redis服务器在执行脚本时被阻塞,无法处理其他请求。
  4. 错误处理机制有限

    • 如果Lua脚本在执行过程中出现错误,Redis不会回滚已经执行的部分,这可能导致数据处于不一致状态。
    • 错误发生后,通常只能通过日志来追踪问题所在,缺乏更高级别的错误恢复机制。
  5. 内存消耗

    • Lua脚本一旦执行就会被缓存起来供后续调用使用,这可以提高性能但同时也增加了内存使用量。如果脚本数量庞大或每个脚本占用较多内存,可能会给Redis带来额外的压力。
  6. 版本兼容性

    • 随着Redis版本的更新,Lua解释器的版本也可能发生变化,这可能会导致旧版本脚本在新版本Redis上不能正常工作的问题。
  7. 安全性考虑

    • 使用Lua脚本时需要注意安全性,避免恶意用户利用脚本执行攻击。例如,应避免直接将用户输入作为脚本的一部分执行,以防代码注入风险。
package com.cokerlk.redisclientside;import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.*;@RestController
public class LuaController {@Resourceprivate StringRedisTemplate stringRedisTemplate;private static final String LUA_SCRIPT = """if tonumber(redis.call('exists', KEYS[1])) == 0 thenredis.call('set', KEYS[1],'10')endif tonumber(redis.call('exists', KEYS[2])) == 0 thenredis.call('sadd', KEYS[2],'-1')endif tonumber(redis.call('get', KEYS[1])) > 0 and tonumber(redis.call('sismember', KEYS[2] , ARGV[1])) == 0  then redis.call('incrby', KEYS[1],'-1') redis.call('sadd',KEYS[2],ARGV[1])return 1else return 0 end""";@GetMapping("/sk")public Map<String,Object> secKill(String pid){Map<String,Object> resp = new HashMap<>();String uid = String.valueOf(new Random().nextInt(100000000));List<String> keys = new ArrayList<>();keys.add("P" + pid); //P1010 String类型 用于保存产品库存量keys.add("U" + pid);//U1010 SET类型 用于保存秒杀确权的UIDDefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LUA_SCRIPT,Long.class);Long result = stringRedisTemplate.execute(redisScript, keys,uid);resp.put("uid", uid);resp.put("result", result);return resp;}
}

Spring Retry + Redis Watch实现乐观锁

Spring Retry 和 Redis 的 WATCH 命令可以结合使用来实现乐观锁,尤其是在处理分布式环境下的并发控制时。这种组合可以有效地减少锁的开销,并提供一种非阻塞的方式来处理并发更新。

实现步骤

  1. 使用 WATCH 监视键: 在开始事务之前,使用 WATCH 命令监视一个或多个键。这告诉Redis在这些键上设置一个“观察点”,如果这些键在事务执行过程中被其他客户端修改,则当前事务将失败。

  2. 发起 MULTI 开始事务: 当所有需要监视的键都已确定后,使用 MULTI 命令开启一个事务。从这一刻起,所有后续命令都会被收集起来,直到 EXEC 被调用。

  3. 尝试执行命令: 在事务中执行所需的命令(例如 GETSET 等),最后通过 EXEC 提交事务。如果自 WATCH 以来没有键被修改,那么事务将成功提交;否则,EXEC 将返回 null 表示事务失败。

  4. 使用 Spring Retry 进行重试: 如果由于其他客户端修改了受监视的键而导致事务失败,可以通过 Spring Retry 来自动重试整个过程。这样,应用程序可以在不增加复杂性的情况下处理并发冲突。

  5. 定义重试逻辑: 需要为 Spring Retry 配置适当的重试策略,包括最大重试次数、等待间隔等参数。同时,应该考虑何时停止重试,比如当达到最大重试次数或者超过某个时间限制时。

添加依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- <dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.5</version></dependency>--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId></dependency><dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies>

 业务逻辑

package com.cokerlk.redisclientside;import jakarta.annotation.Resource;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;
import java.util.Objects;@Service
public class SampleService {@Resourceprivate RedisTemplate<String,Object> redisTemplate;@Retryable(retryFor = IllegalStateException.class, maxAttempts = 2)@Transactionalpublic String saWatch(){System.out.println("executing sa()");List<Object> execute = redisTemplate.execute(new SessionCallback<>() {public List<Object> execute(RedisOperations operations) throws DataAccessException {redisTemplate.watch("sa001");redisTemplate.multi();redisTemplate.opsForValue().set("pri001", -100);try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}redisTemplate.opsForValue().set("sa001", 100);return redisTemplate.exec();}});if(Objects.isNull(execute)){System.out.println("发现并发冲突:" + execute);throw new IllegalStateException("Retry");}else{System.out.println("exec执行成功:" + execute);}return "success";}
}
  • redisTemplate.execute(SessionCallback):

    • 使用 SessionCallback 来定义一个Redis会话,其中包含了一系列命令,这些命令将在一个单独的事务中执行。
  • redisTemplate.watch("sa001"):

    • 开始监视键 "sa001",确保在接下来的事务期间如果该键被其他客户端修改,则当前事务将失败。
  • redisTemplate.multi():

    • 启动一个Redis事务,之后的所有命令都会被收集起来,直到调用 exec()

控制器

package com.cokerlk.redisclientside;import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class SampleController {@Resourceprivate RedisTemplate<String,Object> redisTemplate;@Resourceprivate SampleService sampleService;@GetMapping("/test")public String testWatch(){sampleService.saWatch();return "success";}@GetMapping("/setSA")public String setSA(){redisTemplate.opsForValue().set("sa001",300);return "success";}}

Application

package com.cokerlk.redisclientside;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;@SpringBootApplication
@EnableRetry
public class RedisClientSideApplication {public static void main(String[] args) {SpringApplication.run(RedisClientSideApplication.class, args);}}

测试

###
GET http://localhost:8080/test###
GET http://localhost:8080/setSAexecuting sa()
exec执行成功:[true, true]
executing sa()
exec执行成功:[]

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

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

相关文章

揭秘 Fluss 架构组件

这是 Fluss 系列的第四篇文章了&#xff0c;我们先回顾一下前面三篇文章主要说了哪些内容。 Fluss 部署&#xff0c;带领大家部署Fluss 环境&#xff0c;体验一下 Fluss 的功能Fluss 整合数据湖的操作&#xff0c;体验Fluss 与数据湖的结合讲解了 Fluss、Kafka、Paimon 之间的…

leetcode82:删除链表中的重复元素II

原题地址&#xff1a;82. 删除排序链表中的重复元素 II - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&…

【面试经典】多数元素

链接&#xff1a;169. 多数元素 - 力扣&#xff08;LeetCode&#xff09; 解题思路&#xff1a; 在本文中&#xff0c;“数组中出现次数超过一半的数字” 被称为 “众数” 。 需要注意的是&#xff0c;数学中众数的定义为 “数组中出现次数最多的数字” &#xff0c;与本文定…

AT24C02学习笔记

看手册&#xff1a; AT24Cxx xx代表能写入xxK bit(xx K)/8 byte 内部写周期很关键&#xff0c;代表每一次页写或字节写结束后时间要大于5ms&#xff08;延时5ms确保完成写周期&#xff09;&#xff0c;否则时序会出错。 页写&#xff1a;型不同号每一页可能写入不同大小的…

蓝牙BLE开发——解决iOS设备获取MAC方式

解决iOS设备获取MAC方式 uniapp 解决 iOS 获取 MAC地址&#xff0c;在Android、iOS不同端中互通&#xff0c;根据MAC 地址处理相关的业务场景&#xff1b; 文章目录 解决iOS设备获取MAC方式监听寻找到新设备的事件BLE工具效果图APP监听设备返回数据解决方式ArrayBuffer转16进制…

01 Oracle 基本操作

Oracle 基本操作 初使用步骤 1.创建表空间 2.创建用户、设置密码、指定表空间 3.给用户授权 4.切换用户登录 5.创建表 注意点&#xff1a;oracle中管理表的基本单位是用户 文章目录 了解Oracle体系结构 1.创建表空间**2.删除表空间**3.创建用户4.给用户授权5.切换用户登录6.表操…

独一无二,万字详谈——Linux之文件管理

Linux文件部分的学习&#xff0c;有这一篇的博客足矣! 目录 一、文件的命名规则 1、可以使用哪些字符&#xff1f; 2、文件名的长度 3、Linux文件名的大小写 4、Linux文件扩展名 二、文件管理命令 1、目录的创建/删除 &#xff08;1&#xff09;、目录的创建 ① mkdir…

rust windwos 两个edit框

use winapi::shared::minwindef::LOWORD; use windows::{core::*,Win32::{Foundation::*,Graphics::Gdi::{BeginPaint, EndPaint, PAINTSTRUCT},System::LibraryLoader::GetModuleHandleA,UI::WindowsAndMessaging::*,}, };// 两个全局静态变量&#xff0c;用于保存 Edit 控件的…

解锁成长密码:探寻刻意练习之道

刻意练习&#xff0c;真有那么神&#xff1f; 在生活中&#xff0c;你是否有过这样的困惑&#xff1a;每天苦练英语口语&#xff0c;可一到交流时还是支支吾吾&#xff1b;埋头苦学吉他&#xff0c;却总是卡在几个和弦转换上&#xff1b;工作多年&#xff0c;业务能力却似乎陷入…

WPS中如何为指定区域的表格添加行或者列,同时不影响其它表格?

大家好&#xff0c;我是小鱼。 日常工作中会遇到这种情况&#xff1a;在一个Excel工作表中有多个表格&#xff0c;因为后期数据量增加就需要为指定区域的表格添加行或者列&#xff0c;但是不能影响其它表格。这种情况下我们应该怎么操作呢&#xff1f; 为指定区域的表格添加行…

Gitlab17.7+Jenkins2.4.91实现Fastapi项目持续发布版本详细操作(亲测可用)

一、gitlab设置&#xff1a; 1、进入gitlab选择主页在左侧菜单的下面点击管理员按钮。 2、选择左侧菜单的设置&#xff0c;选择网络&#xff0c;在右侧选择出站请求后选择允许来自webhooks和集成对本地网络的请求 3、webhook设置 进入你自己的项目选择左侧菜单的设置&#xff…

模型工作流:自动化的模型内部三角面剔除

1. 关于自动减面 1.1 自动减面的重要性及现状 三维模型是游戏、三维家居设计、数字孪生、VR/AR等几乎所有三维软件的核心资产&#xff0c;模型的质量和性能从根本上决定了三维软件的画面效果和渲染性能。其中&#xff0c;模型减面工作是同时关乎质量和性能这两个要素的重要工…

Unity微信小游戏接入开放数据域

demo地址&#xff1a;https://github.com/wechat-miniprogram/minigame-unity-webgl-transform/tree/main/Demo/Ranking 官方说明&#xff1a; https://github.com/wechat-miniprogram/minigame-unity-webgl-transform/blob/main/Design/OpenData.md 准备一个Canvas&#xff0c…

如何实现 MySQL 的读写分离?

面试题 你们有没有做 MySQL 读写分离&#xff1f;如何实现 MySQL 的读写分离&#xff1f;MySQL 主从复制原理的是啥&#xff1f;如何解决 MySQL 主从同步的延时问题&#xff1f; 面试官心理分析 高并发这个阶段&#xff0c;肯定是需要做读写分离的&#xff0c;啥意思&#x…

go window安装protoc protoc生成protobuf文件

1. 下载&#xff1a; Releases protocolbuffers/protobuf GitHub 2. 解压缩&#xff1a; 3. 配置环境变量&#xff1a; 选择系统变量->Path -> 新增 解压缩后的bin路径 4. 打印版本&#xff1a; protoc --version 5. 安装protoc-gen-go cmd 下输入安装命令&#xff0…

学习C++:标识符命名规则

标识符命名规则&#xff1a; 作用&#xff1a;C规定给标识符&#xff08;变量、常量&#xff09;命名时&#xff0c;有一套自己的规则 标识符不能是关键字 标识符只能由字母、数字、下划线组成 第一个字符必须为字母或下划线 标识符中字母区分大小写 &#xff08;给标识符命…

Linux系统程序设计--6.线程

线程基本概念 刚创建的进程默认有一个线程&#xff0c;成为主控线程(主线程) Linux线程实现 线程标识 Linux线程的创建和终止 pthread_create 龟兔赛跑案例 #include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<math.h>void * th_fn…

5G学习笔记之Non-Public Network

目录 0. NPN系列 1. 概述 2. SNPN 2.1 SNPN概述 2.2 SNPN架构 2.3 SNPN部署 2.3.1 完全独立 2.3.2 共享PLMN基站 2.3.3 共享PLMN基站和PLMN频谱 3. PNI-NPN 3.1 PNI-NPN概述 3.2 PNI-NPN部署 3.2.1 UPF独立 3.2.2 完全共享 0. NPN系列 1. NPN概述 2. NPN R18 3. 【SNPN系列】S…

DevOps实战:用Kubernetes和Argo打造自动化CI/CD流程(1)

DevOps实战&#xff1a;用Kubernetes和Argo打造自动化CI/CD流程&#xff08;1&#xff09; 架构 架构图 本设计方案的目标是在一台阿里云ECS服务器上搭建一个轻量级的Kubernetes服务k3s节点&#xff0c;并基于Argo搭建一套完整的DevOps CI/CD服务平台&#xff0c;包括Argo CD…

清空DNS 缓存

如果遇到修改了host文件&#xff0c;但是IP和域名的映射有问题的情况&#xff0c;可以尝试刷新DNS缓存。 ipconfig/flushdns win建加R建&#xff0c;然后输入cmd&#xff0c;然后回车 然后回车&#xff0c;或者点击确定按钮。 出现如下所示标识清空DNS 缓存成功。