Springboot高并发乐观锁

news/2024/12/28 12:41:12/

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.ppmy.cn/news/1558787.html

相关文章

国标GB28181-2022平台EasyGBS:安防监控中P2P的穿透方法

在安防监控领域&#xff0c;P2P技术因其去中心化的特性而受到关注&#xff0c;尤其是在远程视频监控和数据传输方面。P2P技术允许设备之间直接通信&#xff0c;无需通过中央服务器&#xff0c;这在提高效率和降低成本方面具有明显优势。然而&#xff0c;P2P技术在实际应用中也面…

vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)

1.安装video-player npm install video.js videojs-player/vue --save在main.js中配置全局引入 // 导入视频播放组件 import VueVideoPlayer from videojs-player/vue import video.js/dist/video-js.cssconst app createApp(App) // 视频播放组件 app.use(VueVideoPlayer)2…

自动驾驶控制算法-横向误差微分方程LQR前馈控制

本文是学习自动驾驶控制算法第六讲 前馈控制与航向误差以及前两节的学习笔记。 1 横向误差微分方程 以规划的轨迹作为自然坐标系&#xff0c;计算自车在轨迹上的投影点&#xff0c;进而计算误差&#xff1a; 如图所示&#xff0c;横向误差为 d d d&#xff0c;航向误差为 θ…

Fast adaptively balanced min-cut clustering

#0.论文信息 标题&#xff1a;Fast adaptively balanced min-cut clustering期刊&#xff1a;Pattern Recognition作者: Feiping Nie , Fangyuan Xie , Jingyu Wang ,Xuelong Li机构: China Telecom, Northwestern Polytechnic al University.代码链接&#xff1a; #1.摘要 …

低空经济的地理信息支撑:构建安全、高效的飞行管理体系

随着无人机等低空飞行器的广泛应用&#xff0c;低空空域管理的重要性日益凸显。地理信息技术作为低空空域管理的重要支撑&#xff0c;对于保障低空经济的健康发展具有不可替代的作用。 地理信息技术在低空空域管理中的作用 地理信息技术在低空空域管理中扮演着关键角色&#x…

科研内训:助力科研成果转化--定制化服务、以需求为导向,深刻理解科研团队在不同领域中的特定需求,从内容设计到实施全流程,确保真正契合的实际需求

科研内训&#xff1a;助力科研成果转化--定制化服务 以需求为导向&#xff0c;深刻理解科研团队在不同领域中的特定需求&#xff0c;从内容设计到实施全流程&#xff0c;确保真正契合的实际需求。 优势&#xff1a;1定制化方案 根据科研方向、团队需求及项目背景量身定制&#…

【hackmyvm】deba靶机wp

tags: HMVnodejs反序列化CVE-2017-5941wine命令定时任务 1. 基本信息^toc 文章目录 1. 基本信息^toc2. 信息收集2.1. 端口扫描2.2. 目录扫描 3. nodejs反序列化 (CVE-2017-5941)4. www-data提权low用户5. 定时任务提权6. wine命令 提权root6.1. 利用CS获取root 靶机链接 http…

Jenkins入门使用

Jenkins入门使用 1先安装jdk才能运行jenkins yum install -y java-1.8.0-openjdk.x86_64 2 安装jenkins&#xff0c;运行&#xff0c;进行端口绑定&#xff0c;启动jenkins docker search jenkins docker pull jenkins/jenkins docker run -d -u root -p 8080:8080 -p 50000:50…