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

简单说说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. 客户端 1 获取节点 A、B、C 上的锁。由于网络问题,无法访问 D 和 E。
  2. 节点C上的时钟向前跳跃,导致锁过期。
  3. 客户端2获取节点C、D、E上的锁。由于网络问题,无法访问A和B。
  4. 客户 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分布式锁

什么是分布式锁 分布式锁&#xff08;多服务共享锁&#xff09;在分布式的部署环境下&#xff0c;通过锁机制来让多客户端互斥的对共享资源进行访问/操作。 为什么需要分布式锁 在单体应用服务里&#xff0c;不同的客户端操作同一个资源&#xff0c;我们可以通过操作系统提供…...

什么是 Java 中的 IO 和 NIO?它们之间有什么区别?什么是 Java 中的内存管理和垃圾回收?常见的垃圾回收算法有哪些?

什么是 Java 中的 IO 和 NIO&#xff1f;它们之间有什么区别&#xff1f; 在 Java 中&#xff0c;IO&#xff08;Input/Output&#xff09;和NIO&#xff08;New IO&#xff09;都是用于处理输入输出操作的API。它们之间有以下区别&#xff1a; IO&#xff08;传统IO&#xff…...

【图论】基环树

基环树其实并不是树&#xff0c;是指有n个点n条边的图&#xff0c;我们知道n个点n-1条边的连通图是树&#xff0c;再加一条边就会形成一个环&#xff0c;所以基环树中一定有一个环&#xff0c;长下面这样&#xff1a; 由基环树可以引申出基环内向树和基环外向树 基环内向树如…...

如何快速捕获和验证用户软件需求,实现快速迭代

在软件开发过程中&#xff0c;快速捕获和验证用户需求&#xff0c;以及迅速迭代功能&#xff0c;是保持项目敏捷性和用户满意度的关键。下面将介绍一些建议&#xff0c;帮助你在软件开发过程中更有效地满足用户需求。 1. 深入沟通与用户互动 要捕获用户需求&#xff0c;必须与…...

爱上算法:每日算法(24-2月4号)

&#x1f31f;坚持每日刷算法&#xff0c;&#x1f603;将其变为习惯&#x1f91b;让我们一起坚持吧&#x1f4aa; 文章目录 [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/)思路CodeJavaC 复杂度 [225. 用队列实现栈](https://leetcode.cn/…...

【Node系列】创建第一个服务器应用

文章目录 一、node介绍二、node创建应用三、node创建应用步骤四、相关链接 一、node介绍 Node.js是一个基于Chrome V8引擎的JavaScript运行环境&#xff0c;可以用于构建高性能的网络应用程序。它采用事件驱动、非阻塞I/O模型&#xff0c;使得程序可以以高效地方式处理并发请求…...

Linux命令基础学习 (2月4日打卡

1. ls - 列出目录内容 命令格式&#xff1a;ls [选项] [文件/目录] 常用选项&#xff1a; -l&#xff1a;以详细列表格式显示-a&#xff1a;显示所有文件&#xff0c;包括以.开头的隐藏文件 2. mkdir - 创建新目录 命令格式&#xff1a;mkdir [选项] 目录名 常用选项&…...

Python 基础知识概览

Python是一种简洁、易学、强大的编程语言&#xff0c;广泛应用于各种领域&#xff0c;包括Web开发、数据分析、人工智能等。本文将介绍一些Python的基础知识&#xff0c;帮助初学者建立对这门语言的基本了解。 1. Python 的简介 Python是一种高级、面向对象、解释型的编程语言…...

Adobe Camera Raw for Mac v16.1.0中文激活版

Adobe Camera Raw for Mac是一款强大的RAW格式图像编辑工具&#xff0c;它能够处理和编辑来自各种数码相机的原始图像。以下是关于Adobe Camera Raw for Mac的一些主要特点和功能&#xff1a; 软件下载&#xff1a;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.建立多轮对话的完整代码&#xff08;根据自己使用的不同代理需要修改端口&#xff08;port&#xff09;&#xff09; 3.修改代码在自己的Pycharm上访问chagpt的api并实现多轮对话&#xff0c;如果不修改是无法成功运行的。需要确定秘钥和端口以保证正常访…...

HarmonyOS远程真机调试方法

生成密钥库文件 打开DevEco Studio&#xff0c;点击菜单栏上的build&#xff0c; 填一些信息点击&#xff0c;没有key的话点击new一个新的key。 生成profile文件 AppGallery Connect (huawei.com) 进入该链接网站&#xff0c;点击用户与访问将刚生成的csr证书提交上去其中需…...

基于SpringBoot的后端导出Excel文件

后端导出Excel&#xff0c;前端下载。 系列文章指路&#x1f449; 系列文章-基于SpringBoot3创建项目并配置常用的工具和一些常用的类 文章目录 后端导出Excel引入依赖写入响应 前端下载后端导出失败和成功返回的内容类型不同&#xff0c;因此需要分别判断。 工具类ServletUti…...

2 月 5 日算法练习- 动态规划

DP&#xff08;动态规划&#xff09;全称Dynamic Programming&#xff0c;是运筹学的一个分支&#xff0c;是一种将复杂问题分解成很多重叠的子问题、并通过子问题的解得到整个问题的解的算法。 在动态规划中有一些概念&#xff1a; n<1e3 [][] &#xff0c;n<100 [][][…...

SpringBoot整合EasyCaptcha图形验证码

简介 EasyCaptcha&#xff1a;https://github.com/ele-admin/EasyCaptcha Java图形验证码&#xff0c;支持gif、中文、算术等类型&#xff0c;可用于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&#xff08;1&#xff09; 不是代表算法运行一次&#xff0c;是常数次 strchar的时间复杂度 #include<stdi…...

SpringBoot实战第三天

今天主要完成了&#xff1a; 新增棋子分类 棋子分类列表 获取棋子分类详情 更新棋子分类 更新棋子分类和添加棋子分类_分组校验 新增棋子 新增棋子参数校验 棋子分类列表查询(条件分页) 先给出分类实体类 Data public class Category {private Integer id;//主键IDNot…...

mysql学习打卡day22

今日成果&#xff1a; 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 动画导出时会有三个文件&#xff1a; .png 、.json 和 .atlas&#xff1a; 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…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

3.3.1_1 检错编码(奇偶校验码)

从这节课开始&#xff0c;我们会探讨数据链路层的差错控制功能&#xff0c;差错控制功能的主要目标是要发现并且解决一个帧内部的位错误&#xff0c;我们需要使用特殊的编码技术去发现帧内部的位错误&#xff0c;当我们发现位错误之后&#xff0c;通常来说有两种解决方案。第一…...

HTML 列表、表格、表单

1 列表标签 作用&#xff1a;布局内容排列整齐的区域 列表分类&#xff1a;无序列表、有序列表、定义列表。 例如&#xff1a; 1.1 无序列表 标签&#xff1a;ul 嵌套 li&#xff0c;ul是无序列表&#xff0c;li是列表条目。 注意事项&#xff1a; ul 标签里面只能包裹 li…...

Spring Boot面试题精选汇总

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南

文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...

全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比

目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec&#xff1f; IPsec VPN 5.1 IPsec传输模式&#xff08;Transport Mode&#xff09; 5.2 IPsec隧道模式&#xff08;Tunne…...

laravel8+vue3.0+element-plus搭建方法

创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

android13 app的触摸问题定位分析流程

一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...