简单说说redis分布式锁
什么是分布式锁
分布式锁(多服务共享锁)在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问/操作。
为什么需要分布式锁
在单体应用服务里,不同的客户端操作同一个资源,我们可以通过操作系统提供的互斥(锁/信号量等等)来提供互斥的能力,保证操作资源的只有一个客户端。
在分布式的情况里,就需要第三方组件来保证对统一资源的操作的互斥。
(下单中,两个人下单,一个人下单请求走订单服务A机器,另一个人下单请求走订单服务B,这样用单体的思维处理就可能不是很合适,需要借用第三方组件配合来实现分布式锁)
分布式锁可以用redis, zookeeper , etcd等等来实现,下面我们简单说说....
redis分布式锁
简单例子
set key value ex/px nx 或setnx
以setnx 为例,可以使用 setnx key value 来进行 "加锁" ( setnx 主要 是nx加了语义:如果存在就不操作,不存在就添加),多个客户端确保只有一个加锁成功去操作统一资源
127.0.0.1:6379[1]> setnx lockObj 1 // 加锁
(integer) 1
127.0.0.1:6379[1]> setnx lockObj 1 // 存在了就不能再加锁
(integer) 0
127.0.0.1:6379[1]> get lockObj
"1"
127.0.0.1:6379[1]> del lockObj // 释放锁
(integer) 1
127.0.0.1:6379[1]> get lockObj
(nil)
127.0.0.1:6379[1]> setnx lockObj 1
(integer) 1

上面就是简单的加锁的例子,仔细思考下分布式锁使用的使用我们需要考虑哪些问题?
存在的问题
简单的总结了下,我们在使用redis分布式锁的时候需要考虑如下情况:
1- 死锁问题
2- 续锁生命周期
3- 操作原子性
4- 锁的归属权
5- redis 集群,锁的状态一致性
死锁问题
如果我们业务代码出现bug或服务器出现问题,没有及时释放锁,那么其他的客户端就永远获取不到这个加锁的资格。
这个时候我们就可以加上对应的处理逻辑:
golang 加上defer 加锁锁逻辑 , python try-Except-finally 用finally 释放锁。并且在加锁的时候加上过期时间(根据业务进行合适的加)
续锁生命周期
上面我们可以解决锁的释放问题,但是我们的业务处理时间不一定百分百能知道处理的时间,这个时候如果锁过期了但是资源操作没有做完,那么就会出现问题。
在java的 Redisson 有个watch 续命机制, golang 的话可以 借鉴 Rllock ,开启一个守护进程监听,定时续命(一定要提前续命,不要等到到时间再续)
操作原子性
在我们进行加锁 加过期时间的时候,这两个操作不能分两步操作。因为如果setnx加锁成功了,这时候失败了,那么这个锁就永远被占用了。
根据这个问题我们可以使用lua脚本或者使用第三方模块是可以同时进行这两个步骤的
`if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('PEXPIRE',KEYS[1],ARGV[2]) else return 0 end`
锁的归属权问题
现在有几个场景:
1- 客户端1 拿到锁处理业务, 没处理完已经过期了;这时候客户端2 拿到锁在处理,结果客户端1 处理完就释放锁了
2- 一个业务不同画像的人处理的业务不同,这时候我们就需要根据不同画像人进行分配 “锁”
我们在实际开发的时候有时候需要了解业务场景, 有时候需要给锁加一个所属权的令牌。可以在setnx key时,key设定特殊化的数值
redis 集群,锁的状态一致性
在redis采用集群(主从),如果master加锁失败了,这时候服务宕机了,slave还同步这个 key ,那么这个时候就会有客户端加锁成功
redis作者对于这个问题提出了解答:REDLOCK
zookeeper 实现分布式锁
简单例子
[zk: localhost:2181(CONNECTED) 62] create /lock
Created /lock
[zk: localhost:2181(CONNECTED) 63] create -s -e /lock/req // 创建临时节点
Created /lock/req0000000000
[zk: localhost:2181(CONNECTED) 64] create -s -e /lock/req
Created /lock/req0000000001
[zk: localhost:2181(CONNECTED) 65] create -s -e /lock/req
Created /lock/req0000000002
[zk: localhost:2181(CONNECTED) 67] ls /lock // 节点下的临时节点
[req0000000000, req0000000001, req0000000002]
[zk: localhost:2181(CONNECTED) 68] delete /lock/req0000000000 // 释放第一锁
golang的例子
package mainimport ("fmt""github.com/samuel/go-zookeeper/zk""sort""time"
)func sortChildren(children []string) {sort.Slice(children, func(i, j int) bool {return children[i] < children[j]})
}
func main() {go func() {conn, _, err := zk.Connect([]string{"xx.xx.xx.xx:2181"}, time.Second*5)if err != nil {fmt.Println("Connect:", err.Error())return}defer conn.Close()lockPath := "/locksObj"lockName := "lock"// 创建锁的根节点_, err = conn.Create(lockPath, []byte{}, int32(0), zk.WorldACL(zk.PermAll))if err != nil && err != zk.ErrNodeExists {fmt.Println("Create:", err.Error())return}// 获取锁lockNodePath, err := conn.CreateProtectedEphemeralSequential(lockPath+"/"+lockName+"-", []byte{}, zk.WorldACL(zk.PermAll))if err != nil {fmt.Println("CreateProtectedEphemeralSequential:", err.Error())return}// doworkfor {children, _, err := conn.Children(lockPath)if err != nil {fmt.Println("Children:", err.Error())return}// 对子节点按照序列号进行排序sortChildren(children)// 检查自己创建的节点是否是第一个节点if lockNodePath == lockPath+"/"+children[0] {// 获取到了锁fmt.Println("Acquired lock")break}// 监听前一个节点的删除事件exists, _, watch, err := conn.ExistsW(lockPath + "/" + children[0])if err != nil {fmt.Println("ExistsW: ", err.Error())break}if !exists {// 前一个节点已删除,再次检查自己创建的节点是否是第一个节点children, _, err = conn.Children(lockPath)if err != nil {fmt.Println("Children: ", err.Error())break}sortChildren(children)if lockNodePath == lockPath+"/"+children[0] {// 获取到了锁fmt.Println("Acquired lock")break}}// 等待前一个节点的删除事件<-watch}// 执行需要保护的代码fmt.Println("====start-1====")time.Sleep(3 * time.Second)fmt.Println(11111111)// 释放锁,删除自己创建的节点err = conn.Delete(lockNodePath, -1)if err != nil {fmt.Println("Delete: ", err.Error())return}fmt.Println("Released lock")}()go func() {conn, _, err := zk.Connect([]string{"xx.xx.xx.xx:2181"}, time.Second*5)if err != nil {fmt.Println("Connect:", err.Error())return}defer conn.Close()lockPath := "/locksObj"lockName := "lock"// 创建锁的根节点_, err = conn.Create(lockPath, []byte{}, int32(0), zk.WorldACL(zk.PermAll))if err != nil && err != zk.ErrNodeExists {fmt.Println("Create:", err.Error())return}// 获取锁lockNodePath, err := conn.CreateProtectedEphemeralSequential(lockPath+"/"+lockName+"-", []byte{}, zk.WorldACL(zk.PermAll))if err != nil {fmt.Println("CreateProtectedEphemeralSequential:", err.Error())return}for {children, _, err := conn.Children(lockPath)if err != nil {fmt.Println("Children:", err.Error())return}// 对子节点按照序列号进行排序sortChildren(children)// 检查自己创建的节点是否是第一个节点if lockNodePath == lockPath+"/"+children[0] {// 获取到了锁fmt.Println("Acquired lock")break}// 监听前一个节点的删除事件exists, _, watch, err := conn.ExistsW(lockPath + "/" + children[0])if err != nil {fmt.Println("ExistsW: ", err.Error())break}if !exists {// 前一个节点已删除,再次检查自己创建的节点是否是第一个节点children, _, err = conn.Children(lockPath)if err != nil {fmt.Println("Children: ", err.Error())break}sortChildren(children)if lockNodePath == lockPath+"/"+children[0] {// 获取到了锁fmt.Println("Acquired lock")break}}// 等待前一个节点的删除事件<-watch}// 执行需要保护的代码fmt.Println("====start-2====")time.Sleep(5 * time.Second)fmt.Println(22222222)// 释放锁,删除自己创建的节点err = conn.Delete(lockNodePath, -1)if err != nil {fmt.Println("Delete: ", err.Error())return}fmt.Println("Released lock")}()time.Sleep(10 * time.Second)
}
zookeeper 怎么实现 分布式锁的

zookeeper 会建立一个长链接,监听锁对象节点的状态和事件
ETCD实现 分布式锁
简单实现
package mainimport ("context""fmt"clientv3 "go.etcd.io/etcd/client/v3""time"
)func main() {go func() {config := clientv3.Config{Endpoints: []string{"xx.xx.xx.xx:2379"},DialTimeout: 5 * time.Second,}// 获取客户端连接client, err := clientv3.New(config)if err != nil {fmt.Println(err)return}// 上锁// 用于申请租约lease := clientv3.NewLease(client)// 申请一个10s的租约leaseGrantResp, err := lease.Grant(context.TODO(), 10) //10sif err != nil {fmt.Println(err)return}// 拿到租约的idleaseID := leaseGrantResp.IDctx, 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")break} else {// 每秒会续租一次fmt.Println("收到自动续租应答", keepResp.ID)}}}()// if key 不存在,then设置它,else抢锁失败kv := clientv3.NewKV(client)// 创建事务txn := kv.Txn(context.TODO())// 如果key不存在txn.If(clientv3.Compare(clientv3.CreateRevision("/lockObj/lock/job"), "=", 0)).Then(clientv3.OpPut("/lockObj/lock/job", "", clientv3.WithLease(leaseID))).Else(clientv3.OpGet("/lockObj/lock/job")) //如果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}// 处理业务fmt.Println("======work======")time.Sleep(5 * time.Second)fmt.Println("======END======")}()time.Sleep(20 * time.Second)
}
实现原理
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发生变化,客户端将收到通知
执行流程
- 步骤 1: 准备
客户端连接 Etcd,以 /lock/mylock 为前缀创建全局唯一的 key,假设第一个客户端对应的 key="/lock/mylock/UUID1",第二个为 key="/lock/mylock/UUID2";客户端分别为自己的 key 创建租约 - Lease,租约的长度根据业务耗时确定,假设为 15s;
- 步骤 2: 创建定时任务作为租约的“心跳”
当一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,key 将因租约到期而被删除,从而锁释放,避免死锁。
- 步骤 3: 客户端将自己全局唯一的 key 写入 Etcd
进行 put 操作,将步骤 1 中创建的 key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 put 操作返回的 Revision 分别为 1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。
- 步骤 4: 客户端判断是否获得锁
客户端以前缀 /lock/mylock 读取 keyValue 列表(keyValue 中带有 key 对应的 Revision),判断自己 key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。
- 步骤 5: 执行业务
获得锁后,操作共享资源,执行业务代码。
- 步骤 6: 释放锁
完成业务流程后,删除对应的key释放锁。
扩展
马丁·克莱普曼 对 分布式锁以及对redlock的看法
分布式锁的目的是确保在可能尝试执行同一工作的多个节点中,只有一个节点实际执行该操作(至少一次只有一个)。
主要有两个功能:
1- 效率:使用锁可以避免不必要地重复相同的工作,多执行一次也无妨,只要最终正确就行
2- 正确性:锁定可以防止并发进程互相干扰并扰乱系统状态。如果锁定失败并且两个节点同时处理同一数据,则会导致文件损坏、数据丢失、永久不一致。
马丁认为锁在分布式系统使用会碰到以下三类问题:
1- 网络延迟:您可以保证数据包始终在某个保证的最大延迟内到达
2- GC问题: 导致锁无法续期等等问题
3- 时钟飘移:依赖于时钟的就容易出现问题
马丁认为redlock 强依赖于时钟,节点之间时钟不对,会使锁不可靠:
假设系统有五个 Redis 节点(A、B、C、D 和 E)和两个客户端(1 和 2)。如果其中一个 Redis 节点上的时钟向前跳动,会发生什么情况?
- 客户端 1 获取节点 A、B、C 上的锁。由于网络问题,无法访问 D 和 E。
- 节点C上的时钟向前跳跃,导致锁过期。
- 客户端2获取节点C、D、E上的锁。由于网络问题,无法访问A和B。
- 客户 1 和 2 现在都相信他们持有锁。
如果 C 在将锁持久保存到磁盘之前崩溃并立即重新启动,则可能会发生类似的问题。因此,Redlock 文档建议延迟重新启动崩溃的节点,至少要延迟最长寿命锁的生存时间。但这种重新启动延迟再次依赖于对时间的相当准确的测量,并且如果时钟跳跃就会失败。
马丁提出了 fencing token 方案

客户端 1 获取租约并获得令牌 33,但随后它进入长时间暂停状态并且租约到期。客户端 2 获取租约,获取令牌 34(数字始终增加),然后将其写入发送到存储服务,包括 34 的令牌。稍后,客户端 1 恢复正常并将其写入发送到存储服务,包括其令牌值 33。但是,存储服务器记得它已经处理了具有更高令牌编号 (34) 的写入,因此它拒绝具有令牌 33 的请求。
总结
分布式锁不是百分百安全,我们要根据实际使用情况来考虑锁的使用(解决效率问题还是正确行问题),在使用分布式锁的时候我们需要考虑锁的续期,锁归属,集群数据一致性,操作原子性,GC,时钟飘逸,网络延迟等等的问题。在cap 理论里, redis保证了ap, zk和etcd保证cp ,所以实际使用中根据业务的情况,选择redis/zk/etcd之一来实现分布式锁。
相关文章:
简单说说redis分布式锁
什么是分布式锁 分布式锁(多服务共享锁)在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问/操作。 为什么需要分布式锁 在单体应用服务里,不同的客户端操作同一个资源,我们可以通过操作系统提供…...
什么是 Java 中的 IO 和 NIO?它们之间有什么区别?什么是 Java 中的内存管理和垃圾回收?常见的垃圾回收算法有哪些?
什么是 Java 中的 IO 和 NIO?它们之间有什么区别? 在 Java 中,IO(Input/Output)和NIO(New IO)都是用于处理输入输出操作的API。它们之间有以下区别: IO(传统IOÿ…...
【图论】基环树
基环树其实并不是树,是指有n个点n条边的图,我们知道n个点n-1条边的连通图是树,再加一条边就会形成一个环,所以基环树中一定有一个环,长下面这样: 由基环树可以引申出基环内向树和基环外向树 基环内向树如…...
如何快速捕获和验证用户软件需求,实现快速迭代
在软件开发过程中,快速捕获和验证用户需求,以及迅速迭代功能,是保持项目敏捷性和用户满意度的关键。下面将介绍一些建议,帮助你在软件开发过程中更有效地满足用户需求。 1. 深入沟通与用户互动 要捕获用户需求,必须与…...
爱上算法:每日算法(24-2月4号)
🌟坚持每日刷算法,😃将其变为习惯🤛让我们一起坚持吧💪 文章目录 [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/)思路CodeJavaC 复杂度 [225. 用队列实现栈](https://leetcode.cn/…...
【Node系列】创建第一个服务器应用
文章目录 一、node介绍二、node创建应用三、node创建应用步骤四、相关链接 一、node介绍 Node.js是一个基于Chrome V8引擎的JavaScript运行环境,可以用于构建高性能的网络应用程序。它采用事件驱动、非阻塞I/O模型,使得程序可以以高效地方式处理并发请求…...
Linux命令基础学习 (2月4日打卡
1. ls - 列出目录内容 命令格式:ls [选项] [文件/目录] 常用选项: -l:以详细列表格式显示-a:显示所有文件,包括以.开头的隐藏文件 2. mkdir - 创建新目录 命令格式:mkdir [选项] 目录名 常用选项&…...
Python 基础知识概览
Python是一种简洁、易学、强大的编程语言,广泛应用于各种领域,包括Web开发、数据分析、人工智能等。本文将介绍一些Python的基础知识,帮助初学者建立对这门语言的基本了解。 1. Python 的简介 Python是一种高级、面向对象、解释型的编程语言…...
Adobe Camera Raw for Mac v16.1.0中文激活版
Adobe Camera Raw for Mac是一款强大的RAW格式图像编辑工具,它能够处理和编辑来自各种数码相机的原始图像。以下是关于Adobe Camera Raw for Mac的一些主要特点和功能: 软件下载:Adobe Camera Raw for Mac v16.1.0中文激活版 RAW格式支持&…...
zabbix自定义监控项
zabbix自定义监控项 1.安装zabbix_get软件 [rootchang local]# yum install zabbix-get2.编辑自定义监控项文件 [rootchang ~]# vim /etc/zabbix/zabbix_agentd.d/cpu.conf UserParametercheck_cpu,top -bn 1 -i -c |grep id |cut -d , -f 4 | tr -d id #UserParameter表示…...
使用Pycharm在本地调用chatgpt的接口
目录 1.安装环境 2.建立多轮对话的完整代码(根据自己使用的不同代理需要修改端口(port)) 3.修改代码在自己的Pycharm上访问chagpt的api并实现多轮对话,如果不修改是无法成功运行的。需要确定秘钥和端口以保证正常访…...
HarmonyOS远程真机调试方法
生成密钥库文件 打开DevEco Studio,点击菜单栏上的build, 填一些信息点击,没有key的话点击new一个新的key。 生成profile文件 AppGallery Connect (huawei.com) 进入该链接网站,点击用户与访问将刚生成的csr证书提交上去其中需…...
基于SpringBoot的后端导出Excel文件
后端导出Excel,前端下载。 系列文章指路👉 系列文章-基于SpringBoot3创建项目并配置常用的工具和一些常用的类 文章目录 后端导出Excel引入依赖写入响应 前端下载后端导出失败和成功返回的内容类型不同,因此需要分别判断。 工具类ServletUti…...
2 月 5 日算法练习- 动态规划
DP(动态规划)全称Dynamic Programming,是运筹学的一个分支,是一种将复杂问题分解成很多重叠的子问题、并通过子问题的解得到整个问题的解的算法。 在动态规划中有一些概念: n<1e3 [][] ,n<100 [][][…...
SpringBoot整合EasyCaptcha图形验证码
简介 EasyCaptcha:https://github.com/ele-admin/EasyCaptcha Java图形验证码,支持gif、中文、算术等类型,可用于Java Web、JavaSE等项目。 添加依赖 <dependency><groupId>com.github.whvcse</groupId><artifactId…...
学习数据结构和算法的第3天
常数循环的复杂度 计算Func4的时间复杂度 voidFunc4(int N) { int count 0; for (int k 0; k < 100; k) { count; } printf("%d\n", count); }O(1) 不是代表算法运行一次,是常数次 strchar的时间复杂度 #include<stdi…...
SpringBoot实战第三天
今天主要完成了: 新增棋子分类 棋子分类列表 获取棋子分类详情 更新棋子分类 更新棋子分类和添加棋子分类_分组校验 新增棋子 新增棋子参数校验 棋子分类列表查询(条件分页) 先给出分类实体类 Data public class Category {private Integer id;//主键IDNot…...
mysql学习打卡day22
今日成果: select * from employees where salary > (select avg(salary) from employees); -- 查询超过平均工资的员工select * from clients where client_id not in (select distinct client_id from invoices); -- 查询没有发票的用户 感谢各位读者查阅&…...
Unity | Spine动画记录
https://blog.csdn.net/linshuhe1/article/details/79792432 https://blog.csdn.net/winds_tide/article/details/128925407 1.需要的三个文件 通常制作好的 Spine 动画导出时会有三个文件: .png 、.json 和 .atlas: skeleton-name.json 或 skeleton-…...
【Flink】FlinkSQL实现数据从MySQL到MySQL
简介 我们在实际开发过程中可以使用Flink实现数据从MySQL传输到MySQL具体操作,本例子Flink版本1.13.6,具体操作如下: 创建mysql测试表 下面语句创建了mysql原表和目标表,并插入一条语句到mysql原表中 CREATE TABLE `mysql_source` ( `id` int(11) unsigned NOT NULL AUT…...
多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...
简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...
微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...
visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
