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

分布式锁

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

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

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

MySQL实现

结构设计

  • 设计表结构:设计一个锁的唯一标识 lock_name 作为表的主键,thread_id 字段存储持有锁的线程ID、设置 counter 字段用于记录重入次数、expires_at 设置锁的过期时间,以防止死锁。
  • 设计索引:还可以在 CREATE 语句中建立联合索引,减少回表次数,优化查询速度,但在高并发场景下执行增删改操作效率会下降。
CREATE TABLE distributed_locks (lock_name VARCHAR(255) PRIMARY KEY,    -- 锁的唯一标识thread_id VARCHAR(255),                -- 当前持有锁的线程IDcounter 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-- 如果锁不存在,设置当前持有者,并将计数器设置为 1redis.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-- 如果锁已被当前持有者持有,将持有者的计数加 1redis.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) thenreturn 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);// 获取锁成功,返回trueif (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方法进行加锁,但是将过期时间默认设置为30sRFuture<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.rhkb.cn/news/468078.html

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

相关文章

CAD 图元 动一下消失

Z.ed.DrawVector(Point3d.Origin, new Point3d(100, 100, 0), 1, true);

【计网】实现reactor反应堆模型 --- 处理数据发回问题 ,异常处理问题

没有一颗星&#xff0c; 会因为追求梦想而受伤&#xff0c; 当你真心渴望某样东西时&#xff0c; 整个宇宙都会来帮忙。 --- 保罗・戈埃罗 《牧羊少年奇幻之旅》--- 实现Reactor反应堆模型 1 数据处理2 数据发回问题3 异常处理问题4 运行效果 1 数据处理 在上一篇文章中我…

Science Robotics 综述揭示演化研究新范式,从机器人复活远古生物!

在地球46亿年的漫长历史长河中&#xff0c;生命的演化过程充满着未解之谜。如何从零散的化石证据中还原古生物的真实面貌&#xff1f;如何理解关键演化节点的具体过程&#xff1f;10月23日&#xff0c;Science Robotics发表重磅综述&#xff0c;首次系统性提出"古生物启发…

string接口的深度理解(内附思维导图)

1. 为什么学习string类&#xff1f; C语言中的字符串 C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列 的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合OOP的思想&#xff0…

python可视化将多张图整合到一起(画布)

这周有点事忙着&#xff0c;没时间重温刚结束的Mathurcup数学建模&#xff0c;这两天也是再看了下&#xff0c;论文还是赶紧挺烂的&#xff0c;但比国赛又有进步&#xff08;说起国赛又不得不抱怨了&#xff0c;基本其余省份都发了&#xff0c;但江西......哎&#xff09;。哎&…

jsp+sevlet+mysql实现用户登陆和增删改查功能

jspsevletmysql实现用户登陆和增删改查功能 一、系统介绍二、功能展示1.用户登陆2.用户列表3.查询用户信息4.添加用户信息5.修改用户信息6.删除用户信息 四、其它1.其他系统实现 一、系统介绍 系统主要功能&#xff1a; 用户登陆、添加用户、查询用户、修改用户、删除用户 二…

Python小白学习教程从入门到入坑------第二十九课 访问模式文件定位操作(语法进阶)

一、访问模式 模式可做操作若文件不存在是否覆盖r只能读报错-r可读可写报错是w只能写创建是w可读可写创建是a只能写创建否&#xff0c;追加写a可读可写创建否&#xff0c;追加写 1.1 r r&#xff1a;只读模式(默认模式)&#xff0c;文件必须存在&#xff0c;不存在就会报错…

TIOBE 编程指数 11 月排行榜公布 VB.Net第九

IT之家 11 月 9 日消息&#xff0c;TIOBE 编程社区指数是一个衡量编程语言受欢迎程度的指标&#xff0c;评判的依据来自世界范围内的工程师、课程、供应商及搜索引擎&#xff0c;今天 TIOBE 官网公布了 2024 年 11 月的编程语言排行榜&#xff0c;IT之家整理如下&#xff1a; P…

聚合联盟的优势

聚合广告联盟对比其他平台优势&#xff1a; 数据透明&#xff0c;自己去平台查看不存在扣量问题。对OVHM做策略优化&#xff0c;帮助开发者做多重点击和下载&#xff0c;使开发者利益最大化。为开发者提供app各大市场上架&#xff0c;隐私协议等指导。 最大的优势就是数据公开…

FakeLocation 版本问题

前言:最新版的FakeLocation 1.3.5 BETA版本在appconfigs.xml文件种添加了绝大多数的应用,导致会返回真实的物理位置&#xff0c;在1.3.2.2都没有这个问题&#xff0c;但是旧版是会被强制更新&#xff0c;不然无法使用. 版本问题/注入/代理 方法 需要使用FakeLocation有二种办法…

算法(第一周)

一周周五&#xff0c;总结一下本周的算法学习&#xff0c;从本周开始重新学习许久未见的算法&#xff0c;当然不同于大一时使用的 C 语言以及做过的简单题&#xff0c;现在是每天一题 C 和 JavaScript&#xff08;还在学&#xff0c;目前只写了一题&#xff09; 题单是代码随想…

大语言模型LLMs在医学领域的最新进展总结

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 相比其他学科&#xff0c;医学AI&#xff0c;是发表学术成果最多的领域。 医学数据的多样性和复杂性&#xff08;包括文本、图像、基因组数据等&#xff09;&#xff0c;使得…

服务器被病毒入侵如何彻底清除?

当服务器遭遇病毒入侵时&#xff0c;彻底清除病毒是确保系统安全和数据完整性的关键步骤。这一过程不仅需要技术上的精准操作&#xff0c;还需要严密的计划、合理的资源调配以及后续的防范措施。以下是一篇关于如何在服务器被病毒入侵时彻底清除病毒的详细指南。 一、初步响应与…

Javascript中如何实现函数缓存?函数缓存有哪些应用场景?

#一、是什么 函数缓存&#xff0c;就是将函数运算过的结果进行缓存 本质上就是用空间&#xff08;缓存存储&#xff09;换时间&#xff08;计算过程&#xff09; 常用于缓存数据计算结果和缓存对象 解释 const add (a,b) > ab; const calc memoize(add); // 函数缓存…

基于LLaMA-Factory微调Llama3

本文简要介绍下基于LLaMA-Factory的llama3 8B模型的微调过程 环境配置 # 1. 安装py3.10虚拟环境 conda create -n py3.10-torch2.2 python3.10 source activate conda activate py3.10-torch2.2# 2. 安装cuda12.2 gpu版torch2.2 conda install pytorch2.2.2 torchvision0.17.…

学习记录:js算法(九十):N皇后

文章目录 N 皇后思路一 N 皇后 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的 n 皇…

RTC精度及校准

RTC精度偏差&#xff1a; RTC的基准时间和精度与石英晶体的频率相关&#xff0c;晶体的谐振频率取决于温度&#xff0c;因此RTC性能与温度相关&#xff0c;晶体的频率偏差是晶体正常频率的温度反转函数。 一、硬件方面&#xff1a; 1.使用高精度振荡器的RTC模块&#xff1b; …

Axure PR 9 多级下拉选择器 设计交互

​ 大家好&#xff0c;我是大明同学。 Axure选择器是一种在交互设计中常用的组件&#xff0c;这期内容&#xff0c;我们来探讨Axure中多级下拉选择器设计与交互技巧。 下拉列表选择输入框元件 创建选择输入框所需的元件 1.在元件库中拖出一个矩形元件。 2.选中矩形元件&…

【设计模式系列】享元模式(十五)

目录 一、什么是享元模式 二、享元模式的角色 三、享元模式的典型应用场景 四、享元模式在ThreadPoolExecutor中的应用 1. 享元对象&#xff08;Flyweight&#xff09;- 工作线程&#xff08;Worker&#xff09; 2. 享元工厂&#xff08;Flyweight Factory&#xff09;- …

LeetCode热题100之贪心算法

1.买卖股票的最佳时机 思路分析&#xff1a;即需要找出某一天的最低价格和它后面几天的最高价格差。 维护一个变量min_price&#xff0c;表示到目前为止遇到的最低股票价格&#xff1b;遍历prices数组&#xff0c;在每一天的价格上&#xff1a; 更新min_price为当前的价格和mi…