当前位置: 首页 > news >正文

【Redis笔记】分布式锁及4种常见实现方法

线程锁

主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如Synchronized、Lock等。

进程锁

控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁

什么是分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁;一个方法在同一时间只能被一个机器的一个线程执行。

分布式锁应具备的条件

  1. 多进程可见
  2. 互斥
  3. 高可用的获取锁与释放锁;
  4. 高性能的获取锁与释放锁;
  5. 具备锁失效机制,防止死锁;
  6. 具备可重入特性;
  7. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败;

分布式锁常见的实现方式

基于Mysql

在数据库中创建一个表,表中包含方法名等字段,并在方法名name字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入一条记录,成功插入则获取锁,删除对应的行就是锁释放。

CREATE TABLE `method_lock` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',PRIMARY KEY (`id`),UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

这里主要是用method_name字段作为唯一索引来实现,唯一索引保证了该记录的唯一性,锁释放就直接删掉该条记录就行了。

INSERT INTO method_lock (method_name) VALUES ('methodName');
delete from method_lock where method_name ='methodName';

缺点

1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能。高并发状态下,数据库读写效率一般都非常缓慢。所以,数据库需要双机部署、数据同步、主备切换;

2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;

3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁。所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

基于Redis分布式锁

获取锁

利用setnx这种互斥命令,利用锁超时时间进行到期释放避免死锁,且Redis具有高可用高性能等特点及优势。

Redis 的分布式锁, setnx 命令并设置过期时间就行吗?

setnx [key] [value] 
expire [key] 30

虽然setnx是原子性的,但是setnx + expire就不是了,也就是说setnx和expire是分两步执行的,【加锁和超时】两个操作是分开的,如果expire执行失败了,那么锁同样得不到释放。

获取锁的原子性问题

# 设置某个 key 的值并设置多少毫秒或秒 过期
set <key> <value> PX <多少毫秒> NX
或
set <key> <value> EX <多少秒> NX
# 设置一个键为lock,值为thread,ex表示以秒为单位,px以微秒为单位,nx表示不存在该key的时候才能设置
set lock thread1 nx ex 10

当且仅当key值lock不存在时,set一个key为lock,val为thread1的字符串,返回1;若key存在,则什么都不做,返回0。

Java的实现

	public boolean tryLock(long timeoutSec) {// 获取线程标识,ID_PREFIX为String threadId = ID_PREFIX +  Thread.currentThread().getId();// 获取锁,name为自定义的业务名称Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}

释放锁

  1. 手动释放
# 将对应的键删除即可
del [key]
  1. 超时释放

释放错误的锁

假设如下三个线程是同一个用户的业务线程,即假设线程1、线程2、线程3申请的分布式锁key一样:

  1. 线程1获取成功了一个分布式锁,由于一些问题,线程1执行超时了,分布式锁被超期释放。
  2. 在锁释放后,有一个线程2又来获取锁,并且成功。
  3. 在线程2执行过程中,线程1运行结束,由于不知道自己锁已经被超期释放,所以它直接手动释放锁,错误的释放了线程2的锁。
  4. 这时如果又有一个线程3前来获取锁,就能获取成功;而线程2此时也持有锁。

所以,设置锁的过期时间时,还需要设置唯一编号。在编程实现释放锁的时候,需要判断当前释放的锁的值是否与之前的一致;若一致,则删除;不一致,则不操作。

代码示例:

	public void unlock() {// 获取线程标识String threadId = ID_PREFIX +  Thread.currentThread().getId();// 获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标识是否一致if (threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}

删除锁的原子性问题

  1. 线程1在时限内完成了业务,它开始执行删除锁的操作。
  2. 线程1判断完当前锁的标识(对应的value)一样后,由于一些问题,线程1被阻塞,该key超期被删除了
  3. 线程2过来申请分布式锁,并且成功
  4. 此时,线程1才正式对分布式锁执行删除,由于可能是同一个用户的业务线程,线程1与线程2的申请的分布式锁key一样,所以线程1调用的删除锁操作将线程2的锁删掉了(悲!故技重施!)…

所以,我们还得确保获取和删除操作之间的原子性。可以借助Lua脚本保证原子性,释放锁的核心逻辑【GET、判断、DEL】,写成 Lua 脚本,让Redis调用。

使用Lua脚本改进Redis释放分布式锁

Lua中Redis的调用函数
redis.call('命令名称','key','其他参数',...)

比如我们执行set name jack命令,可以使用:

redis.call('set','name','jack')
使用Redis调用Lua脚本

调用方法

# script脚本语句;numkeys脚本需要的key类型的参数个数
eval script numkeys key [key ...] arg [arg ...]

例如,执行redis.call('set', 'name', 'Jack')脚本设置redis键值,语法如下:

eval "return redis.call('set', 'name', 'Jack')" 0

如果key和value不想写死,可以使用如下格式

eval "return redis.call('set', KEYS[1], ARGV[1])" 1 name Jack

Lua中,数组下标从1开始,此处1标识只有一个key类型的参数,其他参数都会放入ARGV数组。在脚本中,可以通过KEYS数组和ARGV数组获取参数

Lua脚本(unlock.lua)

if(redis.call('get', KEYS[1]) == ARGV[1]) thenreturn redis.call('del', KEYS[1])
end
return 0
参考完整代码
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock {private String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";// 加载脚本private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();// 加载工程resourcecs下的unlock.lua文件UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}public boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX +  Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}public void unlock() {// 调用Lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX +  Thread.currentThread().getId());}
}

业务调用使用方法

	/*** 业务service导入*/@Resourceprivate RedissonClient redissonClient;/** * 业务方法内*/SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);// 创建锁对象RLock lock = redissonClient.getLock("lock:order:" + userId);// 尝试获取锁boolean isLock = lock.tryLock();// 判断是否成功if (!isLock) {// 获取失败return Result.fail("不允许重复下单");}try {// 需要锁执行的业务代码部分} finally {// 释放锁lock.unlock();}

当前还存在的问题

  1. 不可重入:同一个线程无法多次获取同一把锁(线程1在执行方法1的时候调用了方法2,而方法2也需要锁)
  2. 不可重试:获取锁只尝试一次就返回false,没有重试机制;当前直接返回结果。
  3. 超时释放:锁超时释放虽然可以避免死锁,但是如果业务执行耗时较长,也会导致锁释放,存在安全隐患。
  4. 主从一致性:如果redis提供了主从集群,主存同步存在延迟。当主结点宕机时,从节点尚未同步主结点锁数据,则会造成锁失效。

Redisson框架中就实现了WatchDog(看门狗),加锁时没有指定加锁时间时会启用 watchdog 机制,默认加锁 30 秒,每 10 秒钟检查一次,如果存在就重新设置过期时间。(详细部分可查看http://t.csdnimg.cn/OMxwL)

RedLock应对主从一致性问题

Redis 的作者提出一种解决方案 Redlock ,基于多个 Redis 节点,不再需要部署从库和哨兵实例,只部署主库。但主库要部署多个,官方推荐至少 5 个实例。

流程:

  1. Client先获取「当前时间戳T1」

  2. Client依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁

  3. 如果Client从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败

  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)

  5. 加锁失败,Client向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

为什么要向多个Redis申请锁?

向多台Redis申请锁,即使部分服务器异常宕机,剩余的Redis加锁成功,整个锁服务依旧可用。

为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?

加锁操作的针对的是分布式中的多个节点,所以耗时肯定是比单个实例耗时更,还要考虑网络延迟、丢包、超时等情况发生,网络请求次数越多,异常的概率越大。
所以即使 N/2+1 个节点加锁成功,但如果加锁的累计耗时已经超过了锁的过期时间,那么此时的锁已经没有意义了

释放锁操作为什么要针对所有结点?

为了清除干净所有的锁。在之前申请锁的操作过程中,锁虽然已经加在Redis上,但是在获取结果的时候,出现网络等方面的问题,导致显示失败。所以在释放锁的时候,不管以前有没有加锁成功,都要释放所有节点相关锁。

Zookeeper

ZooKeeper 的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做 Znode。

  1. Client尝试创建一个 znode 节点,比如/lock,比如Client1先到达就创建成功了,相当于拿到了锁

  2. 其它的客户端会创建失败(znode 已存在),获取锁失败。

  3. Client2可以进入一种等待状态,等待当/lock 节点被删除的时候,ZooKeeper 通过 watch 机制通知它

  4. 持有锁的Client1访问共享资源完成后,将 znode 删掉,锁释放掉了

  5. Client2继续完成获取锁操作,直到获取到锁为止

ZooKeeper不需要考虑过期时间,而是用【临时节点】,Client拿到锁之后,只要连接不断,就会一直持有锁。即使Client崩溃,相应临时节点Znode也会自动删除,保证了锁释放。

Zookeeper 是检测客户端是否崩溃

每个客户端都与 ZooKeeper 维护着一个 Session,这个 Session 依赖定期的心跳(heartbeat)来维持。

如果 Zookeeper 长时间收不到客户端的心跳,就认为这个 Session 过期了,也会把这个临时节点删除。

当然这也并不是完美的解决方案

以下场景中Client1和Client2在窗口时间内可能同时获得锁:

  1. Client 1 创建了 znode 节点/lock,获得了锁。

  2. Client 1 进入了长时间的 GC pause。(或者网络出现问题、或者 zk 服务检测心跳线程出现问题等等)

  3. Client 1 连接到 ZooKeeper 的 Session 过期了。znode 节点/lock 被自动删除。

  4. Client 2 创建了 znode 节点/lock,从而获得了锁。

  5. Client 1 从 GC pause 中恢复过来,它仍然认为自己持有锁。

Zookeeper 的优点

  1. 不需要考虑锁的过期时间,使用起来比较方便

  2. watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁

Zookeeper 的缺点

  1. 性能不如 Redis

  2. 部署和运维成本高

  3. 客户端与 Zookeeper 的长时间失联,锁被释放问题

Etcd

Etcd是一个Go语言实现的非常可靠的kv存储系统,常在分布式系统中存储着关键的数据,通常应用在配置中心、服务发现与注册、分布式锁等场景。

Etcd特性

• Lease机制:即租约机制(TTL,Time To Live),etcd可以为存储的kv对设置租约,当租约到期,kv将失效删除;同时也支持续约,keepalive

• Revision机制:每个key带有一个Revision属性值,etcd每进行一次事务对应的全局Revision值都会+1,因此每个key对应的Revision属性值都是全局唯一的。通过比较Revision的大小就可以知道进行写操作的顺序

• 在实现分布式锁时,多个程序同时抢锁,根据Revision值大小依次获得锁,避免“惊群效应”,实现公平锁

• Prefix机制:也称为目录机制,可以根据前缀获得该目录下所有的key及其对应的属性值

• Watch机制:watch支持watch某个固定的key或者一个前缀目录,当watch的key发生变化,客户端将收到通知

满足分布式锁的特性:

• 租约机制(Lease):用于支撑异常情况下的锁自动释放能力

• 前缀和 Revision 机制:用于支撑公平获取锁和排队等待的能力

• 监听机制(Watch):用于支撑抢锁能力

• 集群模式:用于支撑锁服务的高可用

func main() {config := clientv3.Config{Endpoints:   []string{"xxx.xxx.xxx.xxx:2379"},DialTimeout: 5 * time.Second,}// 获取客户端连接client, err := clientv3.New(config)if err != nil {fmt.Println(err)return}// 1. 上锁(创建租约,自动续租,拿着租约去抢占一个key )// 用于申请租约lease := clientv3.NewLease(client)// 申请一个10s的租约leaseGrantResp, err := lease.Grant(context.TODO(), 10) //10sif err != nil {fmt.Println(err)return}// 拿到租约的idleaseID := leaseGrantResp.ID// 准备一个用于取消续租的contextctx, cancelFunc := context.WithCancel(context.TODO())// 确保函数退出后,自动续租会停止defer cancelFunc()// 确保函数退出后,租约会失效defer lease.Revoke(context.TODO(), leaseID)// 自动续租keepRespChan, err := lease.KeepAlive(ctx, leaseID)if err != nil {fmt.Println(err)return}// 处理续租应答的协程go func() {select {case keepResp := <-keepRespChan:if keepRespChan == nil {fmt.Println("lease has expired")goto END} else {// 每秒会续租一次fmt.Println("收到自动续租应答", keepResp.ID)}}END:}()// if key 不存在,then设置它,else抢锁失败kv := clientv3.NewKV(client)// 创建事务txn := kv.Txn(context.TODO())// 如果key不存在txn.If(clientv3.Compare(clientv3.CreateRevision("/cron/lock/job7"), "=", 0)).Then(clientv3.OpPut("/cron/jobs/job7", "", clientv3.WithLease(leaseID))).Else(clientv3.OpGet("/cron/jobs/job7")) //如果key存在// 提交事务txnResp, err := txn.Commit()if err != nil {fmt.Println(err)return}// 判断是否抢到了锁if !txnResp.Succeeded {fmt.Println("锁被占用了:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))return}// 2. 处理业务(锁内,很安全)fmt.Println("处理任务")time.Sleep(5 * time.Second)// 3. 释放锁(取消自动续租,释放租约)// defer会取消续租,释放锁
}

clientv3提供的concurrency包也实现了分布式锁

  1. 首先concurrency.NewSession方法创建Session对象

  2. 然后Session对象通过concurrency.NewMutex 创建了一个Mutex对象

  3. 加锁和释放锁分别调用Lock和UnLock

笔记Zookeeper和Etcd部分参考:https://mp.weixin.qq.com/s/wL9MRnx8HVXNFOt6ZTWELw

相关文章:

【Redis笔记】分布式锁及4种常见实现方法

线程锁 主要用来给方法、代码块加锁。当某个方法或代码使用锁&#xff0c;在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果&#xff0c;因为线程锁的实现在根本上是依靠线程之间共享内存实现的&#xff0c;比如Synchronized、Lock等。 进程锁 控制同…...

SpringMVC第一天

一、SpringMVC简介 1 SpringMVC概述 1.1 SpringMVC概述 SpringMVC是一种基于Java实现MVC模型的轻量级Web框架 优点 使用简单&#xff0c;开发便捷&#xff08;相比于Servlet&#xff09; 灵活性强 2 入门案例【重点】 问题导入 在Controller中如何定义访问路径&#xff…...

如何利用腾讯工蜂提升广告推广和用户运营效率

无代码开发&#xff1a;腾讯工蜂的连接优势 在广告推广和用户运营中&#xff0c;腾讯工蜂的无代码开发优势让广告系统和用户运营系统能够轻松地实现无需API开发的集成。这使得没有专业编程技能的工作人员也能通过腾讯工蜂的用户友好界面&#xff0c;实现系统的快速连接和集成&…...

【QT+QGIS跨平台编译】之三十二:【MiniZip+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文章目录 一、MiniZip介绍二、文件下载三、文件分析四、pro文件五、编译实践一、MiniZip介绍 MiniZip是一个轻量级的开源库,用于创建、读取和操作ZIP文件格式的压缩文件。它提供了一组简单而灵活的API,可以方便地在应用程序中进行ZIP文件的压缩和解压操作。 MiniZip的主要特…...

OLAP技术的发展及趋势简述

这里写自定义目录标题 历史发展基于电子表格的数据分析基于传统数据库的数据分析基于大数据的数据分析 当下的现状OLAP技术的分类MOLAPROLAPHOLAP 主流的OLAP引擎新技术的普及内存向量计算列式数据存储及交换增量查询多源融合计算下推物化视图 发展趋势智能化分析多源融合和自动…...

stupid_brain

前言&#xff1a; 本文用于记录本人AI新手期间犯的各种错误&#xff0c;时常更新。 正文开始&#xff1a; 读取数据的num_worker设置过少&#xff0c;以至于训练速度卡在读取数据上。训练集数据处理&#xff1a;数据增强有利于解决过拟合问题。模型&#xff1a;relu少写、batc…...

C# BackgroundWorker的使用

C# 中的 BackgroundWorker 类是 .NET Framework 提供的一个组件&#xff0c;用于在后台线程上异步执行长时间运行的操作&#xff0c;同时保持与用户界面&#xff08;UI&#xff09;的交互&#xff0c;如更新进度信息或处理取消请求。这使得可以轻松地在不冻结UI的情况下执行耗时…...

(简单有案例)前端实现主题切换、动态换肤的两种简单方式

目录 背景 &#xff08;强烈推荐&#xff09;实现方法1&#xff1a;CSS中原生变量“--”和var() 步骤1&#xff1a;定义css变量 步骤2&#xff1a;使用CSS变量 步骤3&#xff1a;切换主题 实现完整案例 实现方法2&#xff1a;link 动态引入 背景 我们需要做一个功能&#…...

wyh的迷宫

涉及知识点&#xff1a;求迷宫能否到达终点的&#xff0c;而不是求路径数的&#xff0c;用bfs时可以不用重置状态数组&#xff08;回溯&#xff09;。 题目描述 给你一个n*m的迷宫&#xff0c;这个迷宫中有以下几个标识&#xff1a; s代表起点 t代表终点 x代表障碍物 .代…...

AWS云用户创建

问题 需要给工友创建AWS云的用户&#xff0c;这里假设使用分配给自己AWS开发者IAM账号&#xff0c;给别人创建aws IAM账号。 登录系统 打开页面&#xff1a;https://xxx.signin.aws.amazon.com/console&#xff0c;使用分配的开发者账号登录。如下图&#xff1a; 创建用户…...

微信小程序(三十七)选项点击高亮效果

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.选择性渲染类 2.以数字为需渲染内容&#xff08;数量&#xff09; 源码&#xff1a; index.wxml <view class"Area"><!-- {{activeNumindex?Active:}}是选择性添加类名进行渲染 -->&l…...

通过Demo学WPF—数据绑定(二)

准备 今天学习的Demo是Data Binding中的Linq&#xff1a; 创建一个空白解决方案&#xff0c;然后添加现有项目&#xff0c;选择Linq&#xff0c;解决方案如下所示&#xff1a; 查看这个Demo的效果&#xff1a; 开始学习这个Demo xaml部分 查看MainWindow.xaml&#xff1a; …...

数据湖的整体思路

湖本质上是一个集中化&#xff0c;中心化的&#xff0c;一体化的存储技术&#xff0c;并且在其之上追求技术架构的统一化&#xff0c;如流批一体&#xff0c;服务分析一体化。 当数据湖成为中心&#xff0c;那么就可以围湖而建“数据服务环”&#xff0c;环上的服务包括了数仓、…...

51单片机 跑马灯

#include <reg52.h>//毫秒级延时函数 void delay(int z) {int x,y;for(x z; x > 0; x--)for(y 114; y > 0 ; y--); }sbit LED1 P1^0x0; sbit LED2 P1^0x1; sbit LED3 P1^0x2; sbit LED4 P1^0x3; sbit LED5 P1^0x4; sbit LED6 P1^0x5; sbit LED7 P1^0x6; s…...

迎新年年终总结

迎新年年终总结 1、除夕迎新年登高有感 1、除夕迎新年登高有感 除旧岁&#xff0c;迎新年。凭栏立&#xff0c;意阑珊。 天空阔&#xff0c;世道艰。唯自强&#xff0c;可彼岸。 于2024年2月9日 10:51。...

一台服务器可以支持多少TCP连接

前言 ​ 在linux系统中一切皆文件&#xff0c;每当有一个tcp连接建立&#xff0c;那么就会打开一个文件描述符。在Linux系统中&#xff0c;文件描述符打开的个数是有限制的&#xff0c;当超过这个限制的时候内核就会跑出too many open files异常。 ​ linux上能打开的最大文件…...

svg基础(六)滤镜-图像,光照效果(漫反射,镜面反射),组合

1 feImage&#xff1a;图像滤镜 feImage 滤镜从外部来源取得图像数据&#xff0c;并提供像素数据作为输出&#xff08;意味着如果外部来源是一个 SVG 图像&#xff0c;这个图像将被栅格化。&#xff09; 1.1 用法: <feImage x"" y"" width"&quo…...

电脑数据误删如何恢复?9 个Windows 数据恢复方案

无论您是由于软件或硬件故障、网络犯罪还是意外删除而丢失数据&#xff0c;数据丢失都会带来压力和令人不快。 如今的企业通常将其重要数据存储在云或硬盘上。但在执行其中任何一项操作之前&#xff0c;您很有可能会丢失数据。 数据丢失的主要原因是意外删除&#xff0c;任何…...

【doghead】uv_loop_t的创建及线程执行

worker测试程序,类似mediasoup对uv的使用,是one loop per thread 。创建一个UVLoop 就可以创建一个uv_loop_t Transport 创建一个: 试验配置创建一个: UvLoop 封装了libuv的uv_loop_t ,作为共享指针提供 对uv_loop_t 创建并初始化...

云计算运营模式介绍

目录 一、云计算运营模式概述 1.1 概述 二、云计算服务角色 2.1 角色划分 2.1.1 云服务提供商 2.1.2 云服务消费者 2.1.3 云服务代理商 2.1.4 云计算审计员 2.1.5 云服务承运商 三、云计算责任模型 3.1 云计算服务模式与责任关系图 3.2 云计算服务模式与责任关系解析…...

visual studio 2022更改主题为深色

visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中&#xff0c;选择 环境 -> 常规 &#xff0c;将其中的颜色主题改成深色 点击确定&#xff0c;更改完成...

UE5 学习系列(三)创建和移动物体

这篇博客是该系列的第三篇&#xff0c;是在之前两篇博客的基础上展开&#xff0c;主要介绍如何在操作界面中创建和拖动物体&#xff0c;这篇博客跟随的视频链接如下&#xff1a; B 站视频&#xff1a;s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

linux 错误码总结

1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

HBuilderX安装(uni-app和小程序开发)

下载HBuilderX 访问官方网站&#xff1a;https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本&#xff1a; Windows版&#xff08;推荐下载标准版&#xff09; Windows系统安装步骤 运行安装程序&#xff1a; 双击下载的.exe安装文件 如果出现安全提示&…...

C++ 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

[Java恶补day16] 238.除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

蓝桥杯 冶炼金属

原题目链接 &#x1f527; 冶炼金属转换率推测题解 &#x1f4dc; 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V&#xff0c;是一个正整数&#xff0c;表示每 V V V 个普通金属 O O O 可以冶炼出 …...

C语言中提供的第三方库之哈希表实现

一. 简介 前面一篇文章简单学习了C语言中第三方库&#xff08;uthash库&#xff09;提供对哈希表的操作&#xff0c;文章如下&#xff1a; C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...

Vue 模板语句的数据来源

&#x1f9e9; Vue 模板语句的数据来源&#xff1a;全方位解析 Vue 模板&#xff08;<template> 部分&#xff09;中的表达式、指令绑定&#xff08;如 v-bind, v-on&#xff09;和插值&#xff08;{{ }}&#xff09;都在一个特定的作用域内求值。这个作用域由当前 组件…...