【分布式】分布式锁设计与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.xdnf.cn/news/9764.html

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

相关文章

【架构设计常见技术】

EJB EJB是服务器端的组件模型&#xff0c;使开发者能够构建可扩展、分布式的业务逻辑组件。这些组件运行在EJB容器中&#xff0c;EJB将各功能模块封装成独立的组件&#xff0c;能够被不同的客户端应用程序调用&#xff0c;简化开发过程&#xff0c;支持分布式应用开发。 IOC …

万字长文深度解读Movie Gen技术原理(5部曲):图像视频联合生成模型 (2)

​引言 简介 图像和视频基础模型 时间自编码器(TAE) 训练目标 骨干架构 文本嵌入和视觉-文本生成 空间上采样 模型扩展和训练效率 预训练 预训练数据 训练 微调STF 微调数据集创建 监督微调&模型平均 推理 推理提示重写 提高推理效率 评估 评估维度 评估基准…

基于MATLAB的农业病虫害识别研究

matlab有处理语音信号的函数wavread&#xff0c;不过已经过时了&#xff0c;现在处理语音信号的函数名称是audioread选取4.wav进行处理&#xff08;只有4的通道数为1&#xff09; 利用hamming窗设计滤波器 Ham.m function [N,h,H,w] Ham(fp,fs,fc)wp 2*pi*fp/fc;ws 2*pi*…

KEIL编译后直接生成bin文件

KEIL编译后直接生成bin文件 fromelf --bin -o "$LL.bin" "$LL.axf"表示在“与axf相同的文件夹”下生成bin文件。

解析广告联盟的玩法、功能及注意事项

广告联盟是一种商业模式&#xff0c;通过联合多个站点或平台&#xff0c;共同向广告商提供广告展示和推广服务。在这篇文章中&#xff0c;我将重点介绍什么是广告联盟&#xff0c;广告联盟的玩法、功能及注意事项&#xff0c;帮助商业模式策划师更好地了解和应用该模式。 一、…

GitHub中搜索项目方法

0 Preface/Foreword 1 搜索方法 1.1 项目介绍 如上截图&#xff0c;一个项目包含的基本信息&#xff1a; 项目名项目简介项目介绍Watch数量&#xff0c;接收邮件提醒Star数量&#xff0c;关注&#xff0c;subscribeFork数量&#xff0c;在repo中创建分支 1.2 限定项目名查找…

基于java+SpringBoot+Vue的洗衣店订单管理系统设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven mysql5.7或8.0等等组成&#x…

简述kafka集群中的Leader选举机制

Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader&#xff0c;负责管理集群broker 的上下线&#xff0c;所有 topic 的分区副本分配和 Leader 选举等工作。 Controller 的信息同步工作是依赖于 Zookeeper 的。 &#xff08;1&#xff09;创建一个新的 t…

OpenGl绘制了一个雪人

#include <GL/glut.h> #include <math.h>const int n 1000; int q; //圆的半径 int m, p;//圆心 const GLfloat R 0.5f; const GLfloat Pi 3.1415926536f;//初始化OpenGL void init(void) {glClearColor(0.0f, 0.0f, 0.0f, 0.0f);//设置背景颜色glShadeModel(G…

Golang进阶

1.面向对象 1.1.golang语言面向对象编程说明 Golang 也支持面向对象编程(OOP)&#xff0c;但是和传统的面向对象编程有区别&#xff0c;并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。Golang 没有类(class)&#xff0c;Go 语言的结构体(st…

kafka面试夺命连环三十问(上篇)

1、kafka消息发送的流程&#xff1f; 在消息发送的过程中&#xff0c;涉及到两个线程--main线程和sender线程。在main线程中创建了一个双端队列RecordAccumulator。main线程将消息发送给RecordAccumulator&#xff0c;然后sender线程不断从双端队列RecordAccumulator 拉取消息发…

【linux】再谈网络基础(二)

8. 再谈端口号 &#xff08;一&#xff09;与协议之间的关系 端口号(Port)标识了一个主机上进行通信的不同的应用程序 在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识…

关键词策略的有效运用提升内容价值和搜索排名的关键

内容概要 在当今的数字时代&#xff0c;关键词策略是确保内容创作成功的重要基础。无论是个人博客还是商业网站&#xff0c;合适的关键词不仅能够提升文章的可见性&#xff0c;还能显著影响搜索引擎的排名。合理运用关键词&#xff0c;有助于吸引目标读者&#xff0c;将他们引…

1.62亿元!812个项目立项!上海市2024年度“科技创新行动计划”自然科学基金项目立项

本期精选SCI&EI ●IEEE 1区TOP 计算机类&#xff08;含CCF&#xff09;&#xff1b; ●EI快刊&#xff1a;最快1周录用&#xff01; 知网(CNKI)、谷歌学术期刊 ●7天录用-检索&#xff08;100%录用&#xff09;&#xff0c;1周上线&#xff1b; 免费稿件评估 免费匹配期…

【Ant Design Pro】不想用轻量的hook就喜欢用dva的数据状态管理

就像TS是JS的超集一样&#xff0c;antdpro框架也类似&#xff0c;底层也是用dva来构建的。关于数据管理&#xff0c;官方还是建议我们使用轻量的hooks方法来进行操作使用。 使用dva实现数据状态管理效果 框架中的数据管理模式 简单的数据共享 对于简单的应用&#xff0c;不需…

requestAnimationFrame与setInterval的抉择

&#x1f64c; 如文章有误&#xff0c;恳请评论区指正&#xff0c;谢谢&#xff01; ❤ 写作不易&#xff0c;「点赞」「收藏」「转发」 谢谢支持&#xff01; 背景 在之前的业务中遇到有 JS 动画的实现场景&#xff0c;但当电脑打开太多网页或是同时启动很多应用时&#xff0c…

【C++练习】使用海伦公式计算三角形面积

编写并调试一个计算三角形面积的程序 要求&#xff1a; 使用海伦公式&#xff08;Herons Formula&#xff09;来计算三角形的面积。程序需要从用户那里输入三角形的三边长&#xff08;实数类型&#xff09;。输出计算得到的三角形面积&#xff0c;结果保留默认精度。提示用户…

附件商户,用户签到,uv统计功能(geo,bitmap,hyperloglog结构的使用)

目录 附近商户一&#xff1a;Geo数据结构二&#xff1a;附近商户搜索 用户签到一&#xff1a;BitMap功能演示二&#xff1a;实现签到功能三&#xff1a;统计签到功能 uv统计一&#xff1a;hyperloglog的用法二&#xff1a;测试百万数据的tji二&#xff1a;测试百万数据的tji 附…

【LuatOS】修改LuatOS源码为PC模拟器添加高精度时间戳库timeplus

0x00 缘起 LuatOS以及Lua能够提供微秒或者毫秒的时间戳获取工具&#xff0c;但并没有提供获取纳秒的工具。通过编辑LuatOS源码以及相关BSP源码&#xff0c;添加能够获取纳秒的timeplus库并重新编译&#xff0c;以解决在64位Windows操作系统中LuatOS模拟器获取纳秒的问题&#…

[Python学习日记-64] 组合

[Python学习日记-64] 组合 简介 继承与组合 组合的使用 简介 继承其实就是生活当中的归类&#xff0c;就是把对象之间的共同特征再一次提炼&#xff0c;然后形成一个类&#xff0c;但是在实际的开发当中不单单只有归类这一个动作&#xff0c;对象与对象之间都会有一些关系&a…