【分布式】分布式锁设计与Redisson源码解析

news2024/11/6 5:50:23

分布式锁

分布式锁是一种在分布式计算环境中用于控制多个节点(或多个进程)对共享资源的访问的机制。在分布式系统中,多个节点可能需要协调对共享资源的访问,以防止数据的不一致性或冲突。分布式锁允许多个节点在竞争访问共享资源时进行同步,以确保只有一个节点能够获得锁,从而避免冲突和数据损坏。

设计一个分布式锁需要保证以下四大特性:

  • 互斥性:在任意时刻,只能有一个进程持有锁。
  • 进程一致:加锁和解锁的操作必须由同一个进程执行。
  • 防死锁:即使有一个进程在持有锁期间崩溃而未能主动释放锁,必须有其他方式去释放锁,以保证其他进程能够获取到锁。
  • 锁续期:持锁线程执行的操作超出预期时间,只要持锁线程仍然在执行,锁就不应该被释放。

MySQL实现

结构设计

  • 设计表结构:设计一个锁的唯一标识 lock_name 作为表的主键,thread_id 字段存储持有锁的线程ID、设置 counter 字段用于记录重入次数、expires_at 设置锁的过期时间,以防止死锁。
  • 设计索引:还可以在 CREATE 语句中建立联合索引,减少回表次数,优化查询速度,但在高并发场景下执行增删改操作效率会下降。
CREATE TABLE distributed_locks (
    lock_name VARCHAR(255) PRIMARY KEY,    -- 锁的唯一标识
    thread_id VARCHAR(255),                -- 当前持有锁的线程ID
    counter INT DEFAULT 0,                 -- 计数器,记录重入次数
    expires_at TIMESTAMP NULL              -- 锁的过期时间
    # INDEX idx_lock_thread_expires (lock_name, thread_id, expires_at)
);

加锁过程

  1. 首次获取锁:通过 SELECT 语句,以 lock_nameexpires_at 为查询条件,查询存在且未过期的锁。如果锁不存在,则使用 INSERT 语句插入锁标识、线程ID、计数器初始值一和过期时间。如果锁存在,执行下一步骤。(设置过期时间实现**「防死锁」;由于 INSERT 语句默认使用行级锁,同一时刻只能有一个线程插入成功,因此保证了「互斥性」**)

  2. 重复获取锁:判断查询结果中的 thread_id 字段是否与当前线程ID相同。如果相同,说明当前线程需要重复获取锁,执行 UPDATE 语句将 counter 字段加一,并重置过期时间。如果不相同,执行下一步骤。(设置计数器实现可重入锁

  3. 获取锁失败:直接从查询结果返回锁的过期时间,帮助申请锁的线程得知等待锁释放的时间。

-- 开始事务
START TRANSACTION;

-- 查询锁是否存在且未过期
SELECT * FROM distributed_locks 
WHERE lock_name = ? AND expires_at > NOW();

IF 结果为空 THEN
    -- 锁不存在,插入新锁记录
    INSERT INTO distributed_locks (lock_name, thread_id, counter, expires_at)
    VALUES (?, ?, 1, DATE_ADD(NOW(), INTERVAL ? SECOND));
ELSEIF thread_id 等于当前线程ID THEN
    -- 锁已被当前线程持有,重入锁
    UPDATE distributed_locks 
    SET counter = counter + 1, expires_at = DATE_ADD(NOW(), INTERVAL ? SECOND)
    WHERE lock_name = ?;
ELSE
    -- 锁已被其他线程持有,加锁失败
    返回锁的剩余有效期
END IF;

-- 提交事务
COMMIT;

解锁过程

  1. 检查锁持有者:通过 SELECT 语句,以 lock_namethread_id 为查询条件,查询锁是否由当前线程持有。如果结果为空,则返回 NULL 表示解锁失败。如果结果不为空,执行下一步骤。(通过条件判断保证**「进程一致」**,即加解锁为同一线程)
  2. 减少锁计数器:执行 UPDATE 语句给持有锁的线程的计数器减一,并判断计数器是否大于零。如果大于零,说明锁还没有完全释放,执行 UPDATE 语句重置锁的过期时间,返回 0 表示锁未完全释放;如果等于零,说明当前线程已完全释放锁,则执行 DELETE 语句删除整个锁,返回 1 表示锁完全释放。
-- 开始事务
START TRANSACTION;

-- 检查锁是否由当前线程持有
SELECT * FROM distributed_locks 
WHERE lock_name = ? AND thread_id = ?;

IF 结果为空 THEN
    -- 锁不属于当前线程,解锁失败
    返回 NULL;
END IF;

-- 减少锁计数器
UPDATE distributed_locks 
SET counter = counter - 1 
WHERE lock_name = ? AND thread_id = ?;

-- 检查计数器是否大于0
IF counter > 0 THEN
    -- 锁仍然被当前线程持有(重置过期时间)
    UPDATE distributed_locks 
    SET expires_at = DATE_ADD(NOW(), INTERVAL ? SECOND)
    WHERE lock_name = ?;
    返回 0;
ELSE
    -- 计数器为0,完全释放锁
    DELETE FROM distributed_locks WHERE lock_name = ?;
    返回 1;
END IF;

-- 提交事务
COMMIT;

Redis实现

结构设计

  • 选用数据结构:采用 String 结构。设置锁的唯一标识作为 KEY,并指定一个唯一的线程标识作为值 VALUE。

加锁过程

  1. 设置锁:使用 SET 命令 NX(只在键不存在时设置)和 PX(设置过期时间)选项来实现一个原子操作,确保了即使持锁进程崩溃,其他进程仍然能够获取到锁,从而满足**「互斥性」「防死锁」** 。
-- 1.尝试获取锁,值为唯一的线程标识
redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])

解锁过程

  1. 释放锁:通过 DEL 命令清除锁的键来释放锁。在执行 DEL 操作之前,先使用 GET 命令检查锁的值是否与持锁者的唯一标识匹配,从而满足**「进程一致」** 。
-- 2.比较线程标识与锁中的标识是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
	-- 执行del释放锁
	return redis.call('del', KEYS[1])
end
return 0

无论是MySQL还是Redis实现的分布式,虽然都考虑到了互斥性防死锁进程一致问题,但是却无法解决锁续期问题。所以,Redis 官方推荐采用 Redisson 实现 Redis 的分布式锁,借助 Redisson 的 WatchDog 机制能够很好的解决锁续期的问题。

Redisson实现

结构设计

  • 选用数据结构:采用 Hash 结构,设置锁的唯一标识为键,值采用 field-value 格式,以线程ID为 field ,计数器为 value 实现可重入锁。

加锁过程

  1. 执行Lua脚本:整个 Lua 脚本是以事务方式在 Redis 中运行的,由于 Redis 是单线程模型,因此脚本内的所有命令是按顺序一次性执行的,不会在中途被打断或交叉执行,从而保证**「互斥性」**。
  2. 首次获取锁:通过 exists 命令判断锁是否不存在。如果不存在,则执行 hincrby 命令设置 Hash 结构的 field 为线程ID,value 为计数器的初始值一,同时执行 pexpire 命令设置锁的过期时间;如果存在,执行下一步操作。
  3. 重复获取锁:通过 hexists 命令判断锁中的 field 是否与当前线程相同。如果相同,则执行 hincrby 命令给 field 对应的计数器加一,同时执行 pexpire 命令重置锁的过期时间,防止锁在持有者持有期间过期;如果不相同,说明当前锁被其他线程持有。
  4. 返回结果:如果返回 nil 表明获取锁成功;如果返回的数据不为 null 而是 Long,表明申请锁的线程需要等待的时间。

完整代码如下:

-- 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 如果锁不存在,设置当前持有者,并将计数器设置为 1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    -- 设置锁的过期时间,单位为毫秒
    redis.call('pexpire', KEYS[1], ARGV[1])
    -- 返回 nil 表示锁成功创建
    return nil
end

-- 判断锁是否已被当前持有者持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 如果锁已被当前持有者持有,将持有者的计数加 1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    -- 重置锁的过期时间,防止锁在持有者持有期间过期
    redis.call('pexpire', KEYS[1], ARGV[1])
    -- 返回 nil 表示锁成功重入
    return nil
end

-- 如果锁已存在,但被其他持有者持有
-- 返回锁的剩余有效期,单位为毫秒
return redis.call('pttl', KEYS[1])

解锁过程

  1. 检查锁持有者:通过 hexists 命令查询锁中的 field 是否与当前线程相同。如果不相同,表明锁的持有者不是当前线程,返回 nil,如果相同,执行下一步操作。(通过条件判断保证**「进程一致」**,即加解锁为同一线程)
  2. 减少锁计数器:执行 hincrby 命令给持有锁的线程的计数器减一,并判断计数器是否大于零。如果大于零,说明锁还没有完全释放,执行 pexpire 命令重置锁的过期时间,返回 0 表示锁未完全释放;如果等于零,说明当前线程已完全释放锁,则执行 del 删除整个锁,同时执行 publish 命令通知所有等待锁的其他线程,返回 1 表示锁完全释放。(这里执行消息发布是服务于锁等待机制,防止无意义的申请锁而浪费资源)

完整代码如下:

-- 检查锁是否由当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end;

-- 减少当前线程持有的锁计数器
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);

-- 判断计数器值是否大于 0
if (counter > 0) then
    -- 如果计数器大于 0,说明锁仍然被当前线程持有(多次重入)
    -- 重置锁的过期时间,防止锁在当前线程还未完全释放时过期
    redis.call('pexpire', KEYS[1], ARGV[2]);
    -- 返回 0 表示锁还未完全释放(计数器还未清零)
    return 0;
else
    -- 如果计数器等于 0,说明当前线程已完全释放锁
    -- 删除整个锁键
    redis.call('del', KEYS[1]);
    -- 通过发布频道通知锁已释放(适用于等待锁的其他线程)
    redis.call('publish', KEYS[2], ARGV[1]);
    -- 返回 1 表示锁成功释放
    return 1;
end;

-- 若发生意外情况,返回 nil 表示操作失败
return nil;

看门狗机制

当线程尝试执行 tryLock() 方法获取锁时,在内部调用了 tryAcquireAsync() 方法获取锁的等待时间,返回值为 Long 型 。如果返回结果为 null,表明加锁成功;返回结果不为 null,返回值就是需要等待锁的释放时间。

@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // ...
    // 获取加锁的返回值,如果为null则加锁成功,不为null表明加锁失败,还需等待ttl的时间
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // 获取锁成功,返回true
    if (ttl == null) {
        return true;
    }
}

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    // 调用tryAcquireAsync获取锁的等待时间的Long值
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

tryAcquireAsync() 方法中,首先判断锁是否设置了释放时间。

  • 如果设置了锁的释放时间,直接进行上述 lua脚本 的加锁操作,并返回结果;
  • 如果没有设置锁的释放时间,将锁的过期时间设置为默认值30s并进行 lua脚本 的加锁操作,同时启用看门狗机制,不断的进行自动续约,实现**「锁续期」**;
  • 可以看到,两种操作都最终使锁被设置了过期时间,防止持有锁的客户端异常退出后锁无法释放的问题(即**「防死锁」**)。
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    // 如果设置锁的过期时间,直接进行加锁操作返回结果
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 如果没有设置锁的过期时间,同样调用tryLockInnerAsync方法进行加锁,但是将过期时间默认设置为30s
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
			commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        // 出现异常,返回
        if (e != null) {
            return;
        }
        // 锁获取成功,进行自动续约
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

自动续约的操作由 scheduleExpirationRenewal 方法实现。该方法内部首先会从成员变量的 ConcurrentHashMap 集合中根据当前锁的名称获取值,如果获取不到,说明当前线程任务执行完毕,无需再进行锁的自动续期;如果可以获取到值,则启动一个定时任务,通过递归调用实现每 10s 触发一次任务,在任务内部执行了如下的 lua脚本,从而重置锁的过期时间。

-- 检查锁的持有者是否与当前线程相同
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 如果相同,重置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1])
    -- 返回 1 表示操作成功
    return 1  
end

-- 如果 field 不存在,返回 0 表示操作失败
return 0

取消自动续约:当持有锁的线程的任务执行完毕后,会执行 remove() 方法删除 ConcurrentHashMap 集合中的键值,而看门狗在获取 ConcurrentHashMap 集合中的键值失败后,就会返回结果,结束自动续约。

锁等待机制

  1. 尝试获取锁
    • 首先调用 tryAcquire() 方法获取锁剩余的存活时间 ttl,如果结果为 null,返回 true 表明加锁成功。
    • 接着计算当前时间与获取锁之前的时间的差值,如果申请锁的耗时大于等待时间,表明申请锁失败,返回 false。
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();
    // 1.尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // 1.1.锁获取成功
    if (ttl == null) {
        return true;
    }

    // 1.1.申请锁的耗时如果大于等于最大等待时间,则申请锁失败
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }

    current = System.currentTimeMillis();
    // x...
}
  1. 订阅锁释放通知:通过 subscribe 方法,基于当前线程的 threadId 发起一个异步订阅请求,等待锁释放的通知。这一步骤的主要作用是通过订阅锁的释放事件来实现对锁的高效管理,防止无效的锁申请对系统资源造成浪费。
    • 等待锁释放超时:通过 await() 方法(内部使用 CountDownLatch 实现阻塞)在指定时间内等待失败,说明当前线程的等待时间超时,无需再获取锁,需要执行取消订阅和失败处理的逻辑。
    • 取消订阅:通过 cancel() 方法取消订阅。如果取消失败,说明订阅任务正在执行,此时无法直接取消任务。需要执行回调函数等待任务执行完毕;如果取消成功,则执行 acquireFailed() 方法并返回 false。
    • 回调函数取消订阅:通过 onComplete 回调,可以在任务完成后自动触发 unsubscribe 操作,以确保订阅状态被正确清理。
// 2.订阅锁释放通知,通过await方法阻塞等待锁释放,防止无效的锁申请浪费资源
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 2.1.如果await在规定的时间内未完成,表示订阅超时,进入if代码块,执行取消订阅和失败处理的逻辑
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
    if (!subscribeFuture.cancel(false)) {
        subscribeFuture.onComplete((res, e) -> {
            if (e == null) {
                unsubscribe(subscribeFuture, threadId);
            }
        });
    }
    acquireFailed(threadId);
    return false;
}

try {
    // 2.2.计算获取锁的总耗时,如果大于等于最大等待时间,则获取锁失败.
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }
    // 3.x
} finally {
    // 4.x
}
  1. 轮询获取锁
    • 再次获取锁:返回锁的剩余存活时间 ttl;如果 ttl 为空说明获取锁成功,直接返回 true,否则继续下一步。
    • 阻塞获取锁:取锁剩余的存活时间和线程剩余的等待时间的最小值,利用信号量 Semaphore 阻塞获取锁。
// 3.while(true)死循环,不断尝试获取锁
while (true) {
    long currentTime = System.currentTimeMillis();
    // 3.1.再次尝试获取锁
    ttl = tryAcquire(leaseTime, unit, threadId);
    if (ttl == null) {
        return true;
    }
    // 更新剩余的等待时间
    time -= System.currentTimeMillis() - currentTime;
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }
    currentTime = System.currentTimeMillis();
    
    // 3.2.取锁剩余的存活时间和线程剩余的等待时间的最小值,尝试获取锁
    if (ttl >= 0 && ttl < time) {
        getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
    } else {
        getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
    }
    // 更新剩余的等待时间
    time -= System.currentTimeMillis() - currentTime;
    if (time <= 0) {
        acquireFailed(threadId);
        return false;
    }
}
  1. 取消订阅:无论最终是否成功获取锁,在 finally 中都会调用 unsubscribe() 方法取消订阅,以确保资源释放和避免不必要的等待事件。
finally {
    // 4.无论是否获取到了锁,都要取消订阅解锁消息
    unsubscribe(subscribeFuture, threadId);
}

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

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

相关文章

【实测有效】两个Ubuntu系统通过网线传输文件

基本思路是&#xff1a;连接网线后&#xff0c;通过设置静态IP&#xff0c;将两台电脑配置在同一个局域网内&#xff0c;再进行文件传输。 一、物理连接 使用网线将两台电脑的网口连接起来。 二、静态IP设置 两台电脑均需要对各自的静态IP进行设置。 1、查看电脑的IP地址 1&a…

Android camera2

一、序言 为了对阶段性的知识积累、方便以后调查问题&#xff0c;特做此文档&#xff01; 将以camera app 使用camera2 api进行分析。 (1)、打开相机 openCamera (2)、创建会话 createCaptureSession (3)、开始预览 setRepeatingRequest (4)、停止预览 stopRepeating (5)、关闭…

【Redis_Day3】Redis通用命令

【Redis_Day3】Redis通用命令 redis客户端的三种形态redis的快与慢redis通用命令阅读redis官方文档redis中两个核心命令set命令get命令 redis全局命令keys命令&#xff1a;查询当前服务器上的key生产环境 exists命令&#xff1a;判定key是否存在del命令&#xff1a;删除指定的k…

静态库、动态库、framework、xcframework、use_frameworks!的作用、关联核心SDK工程和测试(主)工程、设备CPU架构

1.1库的概念 库&#xff1a;程序代码的集合&#xff0c;编译好的二进制文件加上头文件供使用&#xff0c;共享程序代码的一种方式。 1.2库的分类 根据开源情况分为&#xff1a;开源库&#xff08;能看到具体实现&#xff09;、闭源库&#xff08;只公开调用的的接口&#xf…

小菜家教平台:基于SpringBoot+Vue打造一站式学习管理系统

前言 现在已经学习了很多与Java相关的知识&#xff0c;但是迟迟没有进行一个完整的实践&#xff08;之前这个项目开发到一半&#xff0c;很多东西没学搁置了&#xff0c;同时原先的项目中也有很多的问题&#xff09;&#xff0c;所以现在准备从零开始做一个基于SpringBootVue的…

【C++、数据结构】哈希表——散列表(一)(概念/总结)

「前言」 &#x1f308;个人主页&#xff1a; 代码探秘者 &#x1f308;C语言专栏&#xff1a;C语言 &#x1f308;C专栏&#xff1a; C / STL使用以及模拟实现 &#x1f308;数据结构专栏&#xff1a; 数据结构 / 十大排序算法 &#x1f308;Linux专栏&#xff1a; Linux系统编…

山东路远生态科技有限公司竣工投产仪式暨产品发布会圆满举行

第二十届三中全会于2024年7月15日至18日在北京举行。全会审议通过了《关于进一步全面深化改革、推进中国式现代化的决定》。其中提到,“要健全因地制宜发展新质生产力体制机制”。 新质生产力是由技术革命性突破、生产要素创新性配置、产业深度转型升级而催生的当代先进生产力…

MD5(Crypto)

解题思路 打开文件发现一串代码&#xff0c;结合题目提示&#xff0c;应该是 MD5 加密。 找个在线的 MD5 解密网站&#xff0c;行云流水得到 flag。 题目设计原理 题目设计&#xff1a;无他&#xff0c;MD5 加密。 题目原理&#xff1a; MD5&#xff08;Message-Digest Algo…

EHOME视频平台EasyCVR萤石设备视频接入平台视频诊断技术可以识别哪些视频质量问题?

EasyCVR视频监控汇聚管理平台是一款针对大中型项目设计的跨区域网络化视频监控集中管理平台。萤石设备视频接入平台EasyCVR不仅具备视频资源管理、设备管理、用户管理、运维管理和安全管理等功能&#xff0c;还支持多种主流标准协议&#xff0c;如GB28181、GB35114、RTSP/Onvif…

QML项目实战:自定义Button

目录 一.添加模块 ​1.QtQuick.Controls 2.1 2.QtGraphicalEffects 1.12 二.自定义Button 1.颜色背景设置 2.设置渐变色背景 3.文本设置 4.点击设置 5.阴影设置 三.效果 1.当enabled为true 2.按钮被点击时 3.当enabled为false 四.代码 一.添加模块 1.QtQuick.Con…

实战攻略 | ClickHouse优化之FINAL查询加速

【本文作者&#xff1a;擎创科技资深研发 禹鼎侯】 查询时为什么要加FINAL 我们在使用ClickHouse存储数据时&#xff0c;通常会有一些去重的需求&#xff0c;这时候我们可以使用ReplacingMergeTree引擎。这个引擎允许你存储重复数据&#xff0c;但是在merge的时候会根据order …

labview学习总结

labview学习总结 安装labview的特点一、图形化编程范式二、并行执行机制三、硬件集成能力四、应用领域优势五、开发效率六、系统集成能力**labview基本组成示意图****常用程序结构图解**结语 基础知识介绍界面前后面板的概念平铺式和层叠式 帧的概念结构类型顺序结构for循环whi…

Linux 服务器使用指南:从入门到登录

&#x1f31f;快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 &#x1f31f; &#x1f6a9;博主致力于用通俗易懂且不失专业性的文字&#xff0c;讲解计算机领域那些看似枯燥的知识点&#x1f6a9; 目录 一…

《AI 大模型:重塑软件开发新未来》

引言 在科技的璀璨星河中&#xff0c;AI 大模型宛如一颗耀眼的新星&#xff0c;正以前所未有的力量改写着软件开发的篇章。随着其技术的持续演进&#xff0c;软件开发流程正经历着翻天覆地的变化。从代码自动生成的神奇魔法&#xff0c;到智能测试的精准洞察&#xff0c;AI 大…

acmessl.cn提供接口API方式申请免费ssl证书

目录 一、前沿 二、API接口文档 1、证书可申请列表 简要描述 请求URL 请求方式 返回参数说明 备注 2、证书申请 简要描述 请求URL 请求方式 业务参数 返回示例 返回参数说明 备注 3、证书查询 简要描述 请求URL 请求方式 业务参数 返回参数说明 备注 4、证…

windows server2019下载docker拉取redis等镜像并运行项目

一、基本概念 1、windows server 指由微软公司开发的“Windows”系列中的“服务器”版本。这意味着它是基于Windows操作系统的&#xff0c;但专门设计用于服务器环境&#xff0c;而不是普通的桌面或个人用户使用。主要用途包括服务器功能、用户和资源管理、虚拟化等 2、dock…

Docker-- cgroups资源控制实战

上一篇&#xff1a;容器化和虚拟化 什么是cgroups&#xff1f; cgroups是Linux内核中的一项功能&#xff0c;最初由Google的工程师提出&#xff0c;后来被整合进Linux内核; 它允许用户将一系列系统任务及其子任务整合或分隔到按资源划分等级的不同组内&#xff0c;从而为系统…

解决ImportError: DLL load failed while importing _message: 找不到指定的程序。

C:\software\Anoconda\envs\yolov5_train\python.exe C:\Project\13_yolov5-master\train.py C:\software\Anoconda\envs\yolov5_train\lib\site-packages\torchvision\io\image.py:13: UserWarning: Failed to load image Python extension: [WinError 127] 找不到指定的程序…

超越Axure:探索新一代原型设计工具

Axure RP是一款被广泛认可的快速原型设计工具&#xff0c;专为专业设计师打造&#xff0c;用于创建高效的产品原型图&#xff0c;包括APP和网页的原型图、框架图和结构图等。Axure RP制作的原型图能够实现与实际APP相似的交互效果&#xff0c;便于向用户或客户展示&#xff0c;…

综合项目--博客

一。基础配置&#xff1a; 1.配置主机名&#xff0c;静态IP地址 2.开启防火墙配置 3.部分开启selinux并且配置 4.服务器之间使用同ntp.aliyun.com进行世家能同步 5.服务器之间实现SSH绵密登陆 二。业务需求 1.Sever-NFS-DNS主机配置NFS服务器&#xff0c;将博客网站资源…