Redis--day9--黑马点评--分布式锁(二)

请添加图片描述
(以下所有内容全部来自上述课程)
在这里插入图片描述

分布式锁

1. Redisson功能介绍

基于setnx实现的分布式锁存在下面的问题:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

Redisson是一个在Redis的基础上实现的lava驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
在这里插入图片描述
官网地址:https://redisson.org
GitHub地址:https://github.com/redisson/redisson
jar包下载:https://mvnrepository.com/artifact/org.redisson/redisson/3.50.0

2. Redis入门

  1. 引入依赖:
<dependency>
<groupId>org.redisson</groupId><artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
  1. 配置Redisson客户端:
@Configuration
public class RedisConfig {@Beanpublic RedissonClient redissonClient(){// 配置类Config config =new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址	config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");// 创建客户端return Redisson.create(config);}
}	
  1. 使用Redisson的分布式锁
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson()throws InterruptedException {// 获取锁(可重入),指定锁的名称RLock lock=redissonClient.getLock("anyLock");//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock =lock.tryLock(110,TimeUnit.SECONDS);// 判断释放获取成功if(isLock){try {System.out.println("执行业务");}finally {// 释放锁lock.unlock();}}
}

3. Redisson的可重入锁原理

// 创建锁对象
RLock lock=redissonClient.getLock("lock");@Test
void method1(){boolean isLock = lock.tryLock();if(!isLock){log.error("获取锁失败,1");return;}try{log.info("获取锁成功,1");method2();} finally {log.info("释放锁,1");lock.unlock();}
}
void method2(){boolean isLock =lock.tryLock();if(!isLock){log.error("获取锁失败,2");return;}try {log.info("获取锁成功,2");} finally {log.info("释放锁,2");lock.unlock();}
}

请添加图片描述
获取锁的lua脚本:

local key = KEYS[1]; -- 锁的key
local threadId= ARGV[1];-- 线程唯一标识
local releaseTime =ARGV[2];-- 锁的自动释放时间
--判断是否存在
if(redis.call('exists',key)== 0)then-- 不存在,获取锁redis.call('hset',key, threadId,"1');-- 设置有效期redis.call('expire',key, releaseTime);return 1;--返回结果
end ;
--锁已经存在,判断threadId是否是自己
if(redis.call("hexists",key,threadId)== 1) then--不存在,获取锁,重入次数+1redis.call("hincrby", key, threadId, '1');--设置有效期redis.call('expire',key, releaseTime);return 1;--返回结果
end ;
return 0; --代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的lua脚本:

local key =KEYS[1];-- 锁的key
local threadId= ARGV[1];-- 线程唯一标识
local releseTime=ARGV[2];--锁的自动释放时间
--判断当前锁是否还是被自己持有
if(redis.call("HEXISTs",key,threadId)==0)thenreturn nil;--如果已经不是自己,则直接返回
end ;
--是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
--判断是否重入次数是否已经为0
if(count>0)then--大于0说明不能释放锁,重置有效期然后返回redis.call('EXPIRE',key, releaseTime);return nil;
else--等于0说明可以释放锁,直接删除redis.call('DEL', key);return nil;
end ;

4. Redisson的锁重试和WatchDog机制

tryLock ctrl+alt+B 打开源码

   private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) { //第三处查看//阻塞等待Future结果,就是剩余有效期 把Long接着返回-->第二处查看(往下滑)return get(tryAcquireAsync0(waitTime, leaseTime, unit, threadId));  //查看tryAcquireAsync0(往下滑)}private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {  //第四处查看RFuture<Long> ttlRemainingFuture;if (leaseTime > 0) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {  //命令行已经发出去了,但是结果拿没拿到还不清楚-->FuturettlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,  //查看tryLockInnerAsync(向下滑)TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}  //返回第三处查看(向上滑)CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// lock acquiredif (ttlRemaining == null) {  //剩余有效期=nullif (leaseTime > 0) {  internalLockLeaseTime = unit.toMillis(leaseTime);} else {scheduleExpirationRenewal(threadId);  //自动更新有效期续约 --> 查看scheduleExpirationRenewal(下一个板块的代码)}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}@Overridepublic boolean tryLock() {return get(tryLockAsync());}<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { //第五处查看 return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,"if ((redis.call('exists', KEYS[1]) == 0) " +"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " + //上面是获取成功,下面是没成功"return redis.call('pttl', KEYS[1]);",  //pttl(毫秒的单位)=ttl(秒的单位) 获取指定key的剩余有效期Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));}  //返回第四处(向上滑)@Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { //第二处查看long time = unit.toMillis(waitTime);    //把等待时间转换为毫秒long current = System.currentTimeMillis();  //获取当前时间long threadId = Thread.currentThread().getId();  //获取当前线程id 也就是线程标识Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);  //尝试获取锁 -->看tryAcquire方法(往上滑)// lock acquiredif (ttl == null) {return true;  //获取成功。直接返回}//获取失败,继续尝试(重点!)time -= System.currentTimeMillis() - current;  //现在的时间减去之前的当前时间(就是获取锁消耗的时间)//然后最大等待时间减去消耗的时间,就是剩余等待时间if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;  //没有剩余等待时间,就直接获取失败}//获取失败,继续尝试(但没有立即-->subscribe)current = System.currentTimeMillis();CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);  //订阅了别人释放锁的信号(publish)try {subscribeFuture.get(time, TimeUnit.MILLISECONDS);  //因为时间不确定,所以也是Future} catch (TimeoutException e) {if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(  //最大剩余时间结束-->返回false,继续往下走"Unable to acquire subscription lock after " + time + "ms. " +"Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {subscribeFuture.whenComplete((res, ex) -> {if (ex == null) {  //判断有没有时间了unsubscribe(res, threadId);  //超时,取消订阅}});}acquireFailed(waitTime, unit, threadId);return false;  } catch (ExecutionException e) {acquireFailed(waitTime, unit, threadId);return false;  //获取锁失败}try {time -= System.currentTimeMillis() - current;  //又计算一次等待消耗时间if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;  //套娃,没时间了还是没获取到锁,直接返回获取失败}//小总结:上方一直计算剩余时间,有就直接到这儿,没有就一直重复计算//计算终于有剩余时间(true)while (true) {long currentTime = System.currentTimeMillis();ttl = tryAcquire(waitTime, leaseTime, unit, threadId);  //第一次重试(tryAcquire)// lock acquiredif (ttl == null) {return true;  //成功}//失败就再计算剩余时间time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;  //没有就失败}// waiting for message  失败就继续计算时间currentTime = System.currentTimeMillis();if (ttl >= 0 && ttl < time) {  //ttl和time ,哪个小就先等哪个(time是根本底线,没了也不用等了)//getLatch()  信号量,类似与publish抛出获取信号//ttl是施放时间,更灵活一些,锁释放了就重试(重新获取锁)commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {  //time到期了,还没释放也就不用等了commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}//再获取时间time -= System.currentTimeMillis() - currentTime;if (time <= 0) {  //没时间了,还是直接失败acquireFailed(waitTime, unit, threadId);return false;}}  //时间充足就while(true),返回上面又开始重试} finally {unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);}
//        return get(tryLockAsync(waitTime, leaseTime, unit));}  //至此,重试问题解决  -->  去看超时问题:第四处(向上滑)
  protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();//EntryName:当前锁的名称//一个锁一个entry,第一次来创建新的entry,之后来的返回旧的entry//保证这把锁不管重复了几次,返回的都是同一个entryExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry != null) {oldEntry.addThreadId(threadId);  //同一个线程多次来获取} else {entry.addThreadId(threadId);try {renewExpiration();  //第一次来:续约,更新有效期  -->看renewExpiration} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}}
//renewExpiration:
private void renewExpiration() {  //更新有效期ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());  //先得到entryif (ee == null) {return;}Timeout task = getServiceManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());  //拿出entryif (ent == null) {return;}Long threadId = ent.getFirstThreadId();  //取出线程idif (threadId == null) {return;}CompletionStage<Boolean> future = renewExpirationAsync(threadId);  //刷新有效期-->查看renewExpirationAsyncfuture.whenComplete((res, e) -> {if (e != null) {log.error("Can't update lock {} expiration", getRawName(), e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// reschedule itselfrenewExpiration();  //自己调用自己:递归--重复每十秒更新一次有效期,解决超时问题(重点!)} else {cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);  //内部锁施放时间/3ee.setTimeout(task);}//renewExpirationAsync:
//重置有效期protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +//判断当前线程的锁是不是自己线程拿的--肯定能成功,不是自己线程进不来"redis.call('pexpire', KEYS[1], ARGV[1]); " +//更新有效期"return 1; " +"end; " +"return 0;",Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));}//释放锁的逻辑:
@Overridepublic void unlock() {try {get(unlockAsync(Thread.currentThread().getId()));  //查看unlockAsync} catch (RedisException e) {if (e.getCause() instanceof IllegalMonitorStateException) {throw (IllegalMonitorStateException) e.getCause();} else {throw e;}}//unlockAsync:
@Overridepublic RFuture<Void> unlockAsync(long threadId) {return getServiceManager().execute(() -> unlockAsync0(threadId));}private RFuture<Void> unlockAsync0(long threadId) {CompletionStage<Boolean> future = unlockInnerAsync(threadId);CompletionStage<Void> f = future.handle((opStatus, e) -> {cancelExpirationRenewal(threadId);  //取消更新任务 -->查看:cancelExpirationRenewalif (e != null) {if (e instanceof CompletionException) {throw (CompletionException) e;}throw new CompletionException(e);}if (opStatus == null) {IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "+ id + " thread-id: " + threadId);throw new CompletionException(cause);}return null;});return new CompletableFutureWrapper<>(f);}//cancelExpirationRenewal:
//定时任务的删除
protected void cancelExpirationRenewal(Long threadId) {ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());  //从map中取if (task == null) {return;}if (threadId != null) {task.removeThreadId(threadId);  //先把id干掉}if (threadId == null || task.hasNoThreads()) {Timeout timeout = task.getTimeout();if (timeout != null) {timeout.cancel();  //然后再把任务取消}EXPIRATION_RENEWAL_MAP.remove(getEntryName());  //最后再把entry取消掉}}

请添加图片描述

小总结

Redisson分布式锁原理

  1. 可重入:利用hash结构记录线程id和重入次数
  2. 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
  3. 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

5. Redisson的multiLock原理

请添加图片描述
RedisConfig.java:

package com.hmdp.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissionConfig {@Beanpublic RedissonClient redissonClient(){// 配置类Config config =new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123456");// 创建客户端return Redisson.create(config);}@Beanpublic RedissonClient redissonClient2(){// 配置类Config config =new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://192.168.150.101:6380");// 创建客户端return Redisson.create(config);}@Beanpublic RedissonClient redissonClient3(){// 配置类Config config =new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://192.168.150.101:6381");// 创建客户端return Redisson.create(config);}
}

RedissonTest.java:

package com.hmdp;import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;@Slf4j
@SpringBootTest
class RedissonTest {@Resourceprivate RedissonClient redissonClient;@Resourceprivate RedissonClient redissonClient2;@Resourceprivate RedissonClient redissonClient3;private RLock lock;@BeforeEachvoid setUp(){RLock lock1 = redissonClient.getLock("order");RLock lock2 = redissonClient2.getLock("order");RLock lock3 = redissonClient3.getLock("order");//创建联锁lock = redissonClient.getMultiLock(lock1,lock2,lock3);}@Testvoid method1() throws InterruptedException{//尝试获取锁boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);if (!isLock){log.error("获取锁失败....1");return;}try {log.info("获取锁成功....1");method2();log.info("开始执行业务....1");}finally {log.warn("准备释放锁....1");lock.unlock();}}void method2(){//尝试获取锁boolean isLock = lock.tryLock();if (!isLock){log.error("获取锁失败....2");return;}try {log.info("获取锁成功....2");log.info("开始执行业务....2");}finally {log.warn("准备释放锁....2");lock.unlock();}}
}

总结

  1. 不可重入Redis分布式锁:
    原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
    缺陷:不可重入、无法重试、锁超时失效
  2. 可重入的Redis分布式锁:
    原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
    缺陷:redis宕机引起锁失效问题
  3. Redisson的multiLock:
    原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
    缺陷:运维成本高、实现复杂

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

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

相关文章

ES入门教程 (python 版)

ES入门教程 1. 创建ES对象from elasticsearch import Elasticsearch # 实例化一个ip为localhost&#xff0c;端口为9200&#xff0c;允许超时一小时的es对象 es Elasticsearch(hosts"localhost",port9200,timeout3600) # 1. 创建 索引 index_name "test"…

Gateway中Forward配置+源码观赏

系列文章目录 文章目录系列文章目录一、ForwardPathFilter二、RouteToRequestUrlFilter三、ForwardRoutingFilteryaml forward配置gateway:routes:- id: user-route # uri: lb://useruri: forward:///user/indexpredicates:- Path/user/**- YoGET # filt…

BAS16XV2T1G ON安森美半导体 高速开关二极管 电子元器件IC

BAS16XV2T1G ON Semiconductor 高速开关二极管专业解析1. 产品技术档案BAS16XV2T1G是安森美半导体(ON Semiconductor)推出的高速开关二极管&#xff0c;采用SOT-523超微型封装&#xff08;1.60.80.95mm&#xff09;&#xff0c;专为现代高密度电子设备设计&#xff0c;以其超快…

亲测可用 [安卓]《神秘来电》V1.1无需登入无广告离线打开即用手机模拟发起虚假来电免费版

神秘来电是一款可以模拟虚拟电话的应用程序&#xff0c;它能够在用户需要的时候模拟各种来电&#xff0c;以便用户能够在尴尬的场合脱身。用户可以预设多个不同的来电号码和铃声&#xff0c;并可随时触发这些虚拟电话&#xff0c;在特殊情况下帮助用户摆脱尴尬。它为那些社交恐…

8.20 dp

lc73矩阵置零queue队列标记// 整行置零for(int y0; y<n; y) matrix[i][y] 0; // 整列置零for(int x0; x<m; x) matrix[x][j] 0; class Solution { public:void setZeroes(vector<vector<int>>& matrix) {int m matrix.size(), n matrix[0].size();//…

STL库——string(类模拟实现)

ʕ • ᴥ • ʔ づ♡ど &#x1f389; 欢迎点赞支持&#x1f389; 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言&#xff1b; 文章目录 前言 一、基本框架 二、构造函数 三、析构函数 四、拷贝构造 五、运算符重载 5.1、赋值重载 5.2…

Linux I/O 多路复用实战:深入剖析 Select 与 Poll

## 引言:从“阻塞”的餐厅到“事件驱动”的盛宴 想象一下,你是一家小餐馆的服务员。餐厅只有5张桌子。你的工作流程是这样的:走到1号桌,问他们是否要点菜,然后站在那里等他们决定;等他们点完,再去2号桌,同样站在那里等... 如果1号桌的客人看菜单看了半个小时,那么其他…

【clion】cmake脚本1:调试脚本并构建Fargo项目win32版本

调试脚本并构建 【clion】visual studio的sln转cmakelist并使用clion构建32位 报错 "D:\Program Files\JetBrains\CLion 2022.3.1\bin\cmake\win\x64\bin\cmake.exe" --debugger --debugger-pipe=\\<

VS2005里的快捷键

VS2005是微软在2005发布的一款支持C、C#、.net开发语言的集成开发工具&#xff0c;它支持的C版本为C03&#xff0c;但不支持C11&#xff0c;到VS2013才支持大部分的C11(简称C11)&#xff0c;到VS2015 update3才完全支持C11。既然VS2005不支持C11&#xff0c;而智能指针是C11才引…

前后端联合实现文件下载,实现 SQL Server image 类型文件下载

1、前端 Vue3QualityFile.vue<script setup lang"ts" name"QualityFile"> ...... // 下载&#xff0c;实现 SQL Server image 类型文件下载 const onDownloadClick async (fileNo: string) > {// const result await qualityFileDownloadFileWi…

【OneAI】使用Rust构建的轻量AI网关

LLM网关 统一大模型API入口&#xff0c;使用一个令牌调用多家模型&#xff0c;无需切换API Key兼容OpenAI输入输出规范内置10提供商和50模型&#xff0c;开箱即用支持自动负载、限流、IP限制、Token用量限制等功能支持钉钉、飞书、企微消息预警支持对不同提供商设置代理支持主…

Jenkins服务器配置SSH

1. 创建Jenkins用户的SSH配置ssh-keygen -t rsa -b 4096 -f /tmp/jenkins_ssh_key -N ""2. 在Jenkins服务器上执行以下命令# 切换到root用户 sudo su -# 创建Jenkins用户的SSH目录 mkdir -p /var/lib/jenkins/.ssh chown jenkins:jenkins /var/lib/jenkins/.ssh chmo…

nginx-下载功能-状态统计-访问控制

nginx-下载功能-状态统计-访问控制一、利用nginx做网站提供下载功能1. 进入nginx存放配置文件目录2. 编辑nginx.conf文件&#xff0c;开启下载功能3. 检查nginx.conf主配置文件是否正确4. 重启nginx服务5. 修改首页文件index.html6. 访问首页7. 去网页根目录下新建download目录…

GitLab CI/CD、Jenkins与GitHub Actions在Kubernetes环境中的方案对比分析

GitLab CI/CD、Jenkins与GitHub Actions在Kubernetes环境中的方案对比分析 随着容器化和微服务的普及&#xff0c;基于Kubernetes的部署已经成为主流。在实际的生产环境中&#xff0c;如何选择合适的CI/CD流水线方案以实现自动化构建、测试、部署和发布&#xff0c;直接关系到团…

tcp会无限次重传吗

tcp作为面向连接的&#xff0c;可靠的&#xff0c;字节流。最重要的特点就是可靠&#xff0c;其中重传又是保证可靠的重要前提。那么当tcp发送数据之后&#xff0c;收不到ack的情况下&#xff0c;会无限次重传吗。不会。# cat /proc/sys/net/ipv4/tcp_retries1 3 # cat /proc/s…

EasyAIoT平台部署

EasyAIoT官方文档专注于 AIoT 智能硬件与工业软件解决方案&#xff0c;提供从设备接入到云端管理的全栈服务http://pro.basiclab.top:9988/

功能测试相关问题

1.功能测试流程&#xff08;工作流程&#xff09;需求分析 -- 测试点分析&#xff08;xmind&#xff09;-- 编写测试计划/用例及评审 -- 执行测试用例&#xff08;开发提交测试&#xff09;-- 发现缺陷通过缺陷管理工具提交 -- 回归测试及bug验证&#xff08;开发提测新版本&am…

微服务网关中数据权限传递的那些坑:从 Feign 兼容性问题到解决方案

在微服务架构中&#xff0c;网关作为流量入口&#xff0c;常常需要承担身份认证、权限校验等职责。其中&#xff0c;用户数据权限的传递看似简单&#xff0c;却隐藏着不少兼容性陷阱。本文将结合实际项目经验&#xff0c;聊聊如何解决 Feign 调用时请求头中 JSON 数据的传递问题…

基于SpringBoot的旅游攻略系统网站【2026最新】

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

thingsboard 服务器在2核CPU、2G内存资源配置下如何调优提速,适合开发/演示

物联网设备管理平台致力于为客户提供高效、可靠的物联网解决方案。基于开源物联网平台进行深度二次开发&#xff0c;我们打造了功能强大、灵活易用的物联网平台&#xff0c;广泛应用于智能家居、智能工厂、智能城市等多个领域 一、标准资源要求 CPU&#xff1a;建议至少 8 vCP…