一篇讲透如何理解数据库并发控制

01数据库并发控制的作用

1.1 事务的概念

在介绍并发控制前,首先需要了解事务。数据库提供了增删改查等几种基础操作,用户可以灵活地组合这几种操作,实现复杂的语义。在很多场景下,用户希望一组操作可以做为一个整体一起生效,这就是事务。事务是数据库状态变更的基本单元,包含一个或多个操作(例如多条SQL语句)。

经典的转账事务,就包括三个操作:(1)检查A账户余额是否足够。(2)如果足够,从A扣减100块。(3)B账户增加100块。

事务有个基本特性:这一组操作要么一起生效,要么都不生效,事务执行过程中如遇错误,已经执行的操作要全部撤回,这就是事务的原子性

如果失败发生后,部分生效的事务无法撤回,那数据库就进入了不一致状态,与真实世界的事实相左。例如转账事务从A账户扣款100块后失败了,B账户还未增加款项,如果A账户扣款操作未撤回,这个世界就莫名奇妙丢失了100块。原子性可以通过记日志(更改前的值)来实现,还有一些数据库将事务操作缓存在本地,如遇失败,直接丢弃缓存里的操作。

事务只要提交了,它的结果就不能改变了,即使遇到系统宕机,重启后数据库的状态与宕机前一致,这就是事务的持久性

数据只要存储非易失存储介质,宕机就不会导致数据丢失。因此数据库可以采用以下方法来保证持久性:(1)事务完成前,所有的更改都保证存储到磁盘上了。或(2)提交完成前,事务的更改信息,以日志的形式存储在磁盘,重启过程根据日志恢复出数据库系统的内存状态。一般而言,数据库会选择方法(2),原因留给读者思考。

数据库为了提高资源利用率和事务执行效率、降低响应时间,允许事务并发执行。但是多个事务同时操作同一对象,必然存在冲突,事务的中间状态可能暴露给其它事务,导致一些事务依据其它事务中间状态,把错误的值写到数据库里。需要提供一种机制,保证事务执行不受并发事务的影响,让用户感觉,当前仿佛只有自己发起的事务在执行,这就是隔离性

隔离性让用户可以专注于单个事务的逻辑,不用考虑并发执行的影响。数据库通过并发控制机制保证隔离性。由于隔离性对事务的执行顺序要求较高,很多数据库提供了不同选项,用户可以牺牲一部分隔离性,提升系统性能。这些不同的选项就是事务隔离级别

数据库反映的是真实世界,真实世界有很多限制,例如:账户之间无论怎么转账,总额不会变等现实约束;年龄不能为负值,性别最多只能有男、女、跨性别者三种选项等完整性约束。事务执行,不能打破这些约束,保证事务从一个正确的状态转移到另一个正确的状态,这就是一致性

不同与前三种性质完全由数据库实现保证,一致性既依赖于数据库实现(原子性、持久性、隔离性也是为了保证一致性),也依赖于应用端编写的事务逻辑。

1.2 事务并发控制如何保证隔离性

为了保证隔离性,一种方式是所有事务串行执行,让事务之间不互相干扰。但是串行执行效率非常低,为了增大吞吐,减小响应时间,数据库通常允许多个事务同时执行。因此并发控制模块需要保证:事务并发执行的效果,与事务串行执行的效果完全相同(serializability),以达到隔离性的要求。

为了方便描述并发控制如何保证隔离性,我们简化事务模型。事务是由一个或多个操作组成,所有的操作最终都可以拆分为一系列读和写。一批同时发生的事务,所有读、写的一种执行顺序,被定义为一个schedule,例如:

T1、T2同时执行,一个可能的schedule:
T1.read(A),T2.read(B),T1.write(A),T1.read(B),T2.write(A)

如果并发事务执行的schedule效果与串行执行的schedule(serial schedule)等价,就可以满足serializability。一个schedule不断调换读写操作的顺序,总会变成一个serializable schedule,但是有的调换可能导致事务执行的结果不一样。一个schedule中,相邻的两个操作调换位置导致事务结果变化,那么这两个操作就是冲突的。冲突需要同时满足以下条件:

  • 这两个操作来自不同事务
  • 至少有一个是写操作
  • 操作对象相同

因此常见的冲突包括:

  • 读写冲突。事务先A读取某行数据、事务B后修改该行数据,和事务B先修改某行事务、事务A后读该行记录两种schedule。事务A读到的结果不同。这种冲突可能会导致不可重复读异象和脏读异象。
  • 写读冲突。与读写冲突产生的原因相同。这种冲突可能会导致脏读异象。
  • 写写冲突。两个操作先后写一个对象,后一个操作的结果决定了写入的最终结果。这种冲突可能会导致更新丢失异象。

数据库只要保证,并发事务的schedule,保持冲突操作的执行顺序不变,只调换不冲突的操作,可以成为serial schedule,就可以认为它们等价。

这种等价判断方式叫做conflict equivalent:两个schedule的冲突操作顺序相同。例如下图的例子,T1 write(A)与T3 read(A)冲突,且T1先于T3发生。T1 read(B)和 T2 write(B)冲突,且T2先于T1,因此左图事务执行的schedule,与T2,T1,T3串行执行的serial schedule(右图) 等价。左图的执行顺序满足conflict serializablity。
image.png
image.png
再分析一个反例:T1 read(A)与T2 write(A)冲突且T1先于T2,T2 write(A)与T2 write(A)冲突且T2先于T1。下图这个个schedule无法与任何一个serial schedule等价,是一个不满足conflict serializablity的执行顺序,会造成更新丢失的异象。
image.png
总体来说,serializability是比较严格的要求,为了提高数据库系统的并发性能,很多用户愿意去降低隔离性的要求以寻求更好的性能。数据库系统往往会实现多种隔离级别,供用户灵活选择,关于事务隔离级别,可以参看这篇文章。

并发控制的要求清楚了,如何实现呢?后文将依据冲突检测的乐观程度,一一介绍并发控制常见的实现方法。

02基于两阶段锁的并发控制

2.1 2PL

既然要保证操作按正确的顺序执行,最容易想到的方法就是加锁保护访问对象。数据库系统的锁管理器模块,专门负责给访问对象加锁和释放锁,保证只有持有锁的事务,才能操作相应的对象。锁可以分为两类:S-Lock和X-Lock,S-Lock是读请求使用的共享锁,X-Lock是写请求使用的排他锁。它们的兼容性如下:操作同一个对象,只有两个读请求相互兼容,可以同时执行,读写和写写操作都会因为锁冲突而串行执行。
1.png
2PL(Two-phase locking)是数据库最常见的基于锁的并发控制协议,顾名思义,它包含两个阶段:

  • 阶段一:Growing,事务向锁管理器请求它需要的所有锁(存在加锁失败的可能)。
  • 阶段二:Shrinking,事务释放Growing阶段获取的锁,不允许再请求新锁。

为什么加锁和放锁要泾渭分明地分为两个阶段呢?

2PL并发控制目的是为了达到serializable,如果并发控制不事先将所有需要的锁申请好,而是释放锁后,还允许再次申请锁,可能出现事务内两次操作同一对象之间,其它事务修改这一对象(如下图所示),进而无法达到conflict serializable,出现不一致的现象(下面的例子是lost update)。
2.png
2PL可以保证conflict serializability,因为事务必须拿到所有需要的锁才能执行。例如正在执行的事务A与事务B冲突,事务B要么已经执行完,要么还在等待。因此那些冲突操作的执行顺序,与BA或AB串行执行时冲突操作执行顺序一致。

所以,数据库只要采用2PL就能保证一致性和隔离性了吗?来看一下这个例子:
3.png
以上执行顺序是符合2PL的,但T2读到了未提交的数据。如果此时T1回滚,则会引发级联回滚(T1的更改,不能被任何事务看到)。因此,数据库往往使用的是加强版的S(trong)S(trict)2PL,它相较于2PL有一点不同:shrinking阶段,只能在事务结束后再释放锁,完全杜绝了事务未提交的数据被读到。

2.2 死锁处理

并发事务加锁放锁必然绕不开一个问题--死锁:事务1持有A锁等B锁,事务2持有B锁等A锁。目前解决死锁问题有两种方案:

  • Deadlock Detection:

数据库系统根据waits-for图记录事务的等待关系,其中点代表事务,有向边代表事务在等待另一个事务放锁。当waits-for图出现环时,代表死锁出现了。系统后台会定时检测waits-for图,如果发现环,则需要选择一个合适的事务abort。
image.png

  • Deadlock Prevention:

当事务去请求一个已经被持有的锁时,数据库系统为防止死锁,杀死其中一个事务(一般持续越久的事务,保留的优先级越高)。这种防患于未然的方法不需要waits-for图,但提高了事务被杀死的比率。

2.3 意向锁

如果只有行锁,那么事务要更新一亿条记录,需要获取一亿个行锁,将占用大量的内存资源。

我们知道锁是用来保护数据库内部访问对象的,这些对象根据大小可能是:属性(Attribute)、记录(Tuple)、页面(Page)、表(Table),相应的锁可分为行锁、页面锁、表锁(没人实现属性锁,对于OLTP数据库,最小的操作单元是行)。对于事务来讲,获得最少量的锁当然是最好的,比如更新一亿条记录,或许加一个表锁就足够了。

层次越高的锁(如表锁),可以有效减少对资源的占用,显著减少锁检查的次数,但会严重限制并发。层次越低的锁(如行锁),有利于并发执行,但在事务请求对象多的情况下,需要大量的锁检查。数据库系统为了解决高层次锁限制并发的问题,引入了意向(Intention)锁的概念:

  • Intention-Shared (IS):表明其内部一个或多个对象被S-Lock保护,例如某表加IS,表中至少一行被S-Lock保护。
  • Intention-Exclusive (IX):表明其内部一个或多个对象被X-Lock保护。例如某表加IX,表中至少一行被X-Lock保护。
  • Shared+Intention-Exclusive (SIX):表明内部至少一个对象被X-Lock保护,并且自身被S-Lock保护。例如某个操作要全表扫描,并更改表中几行,可以给表加SIX。读者可以思考一下,为啥没有XIX或XIS

意向锁和普通锁的兼容关系如下所示:
image.png
意向锁的好处在于:当表加了IX,意味着表中有行正在修改。

(1)这时对表发起DDL操作,需要请求表的X锁,那么看到表持有IX就直接等待了,而不用逐个检查表内的行是否持有行锁,有效减少了检查开销。(2)这时有别的读写事务过来,由于表加的是IX而非X,并不会阻止对行的读写请求(先在表上加IX,再去记录上加S/X),事务如果没有涉及已经加了X锁的行,则可以正常执行,增大了系统的并发度。

3303基于Timing Order(T/O)的并发控制

为每个事务分配timestamp,并以此决定事务执行顺序。当事务1的timestamp小于事务2时,数据库系统要保证事务1先于事务2执行。timestamp分配的方式包括:(1)物理时钟;(2)逻辑时钟;(3)混合时钟。

3.1 Basic T/O

基于T/O的并发控制,读写不需加锁, 每行记录都标记了最后修改和读取它的事务的timestamp。当事务的timestamp小于记录的timestamp时(不能读到”未来的”数据),需要abort后重新执行。假设记录X上标记了读写两个timestamp:WTS(X)和RTS(X),事务的timestamp为TTS,可见性判断如下:
读:

  • TTS < WTS(X):该对象对该事务不可见,abort事务,取一个新timestamp重新开始。
  • TTS > WTS(X):该对象对事务可见,更新RTS(X) = max(TTS,RTS(X))。为了满足repeatable read,事务复制X的值。
  • 为了防止读到脏数据,可以在记录上做特殊标记,读请求需等待事务提交后再去读。

写:

  • TTS < WTS(X) || TTS < RTS(X):abort事务,重新开始。
  • TTS > WTS(X) && TTS > RTS(X):事务更新X,WTS(X) = TTS。

这里之所以要求TTS > RTS(X),是为了防止如下情况:读请求的时间戳为rts,已经读过X,时间戳设为RTS(X)=rts,如果新事务的TTS < RTS(X),并且更新成功,则rts读请求再来读一次就看到新的更改了,违反了repeatable read,因此这是为了避免读写冲突。记录上存储了最后的读写时间,可以保证conflict serializable

这种方式也能避免write skew,例如:初始状态,X和Y两条记录,X=-3,Y=5,X+Y >0,RTS(X)=RTS(Y)=WTS(X)=WTS(Y)=0。事务T1的时间戳为TTS1=1,事务T2的时间戳TTS2=2。
image.png
它缺陷包括:

  • 长事务容易饿死,因为长事务的timestamp偏小,大概率会在执行一段时间后读到更新的数据,导致abort。
  • 读操作也会产生写(写RTS)。

04基于Validation(OCC)的并发控制

执行过程中,每个事务维护自己的写操作(Basic T/O在事务执行过程中写就将数据写入DB)和相应的RTS/WTS,提交时判断自己的更改是否和数据库中已存在的数据冲突,如果不冲突才写入DB。OCC分为三个阶段:

  • Read & Write Phase:即读写阶段,事务维护读的结果和即将提交的更改,以及写入记录的RTS和WTS。
  • Validation Phase:检查事务是否与数据库中的数据冲突。
  • Write Phase:不冲突就写入,冲突就abort,restart。

Read & Write Phase结束,进入Validation Phase相当于事务准备完成,进入提交阶段了,进入Validation Phase的时间被选做记录行的时间戳,来定序。不用事务开始时间是因为:事务执行时间可能较长,导致后开始的事务可能先提交,这会加大事务冲突的概率,较小时间戳的事务后写入数据库,肯定会abort。

Validation过程

假设当前只有两个事务T1和T2,并修改了相同数据行,T1的时间戳 < T2的时间戳(即validation顺序:T1 < T2,对用户而言,T1先发生于T2),则有如下情况:

(1)T1在validate阶段,T2还在Read & Write Phase。此时只要T1和T2已经发生的读写没有冲突,就可以提交。

  • 如果WS(T1) ∩ (RS(T2) ∪ WS(T2)) = ∅,说明T2和T1写的记录无冲突,validation通过,可以写入。
  • 否则,T2与T1之间存在读写冲突或写写冲突,T1需要回滚。读写冲突:T2读到了T1写之前的版本,T1提交后,它可能读到T1写的版本,不可重复读。写写冲突:T2有可能在旧版本基础上更新,再次写入,造成T1的更新丢失。

(2)T1完成validate阶段,进入write阶段直到提交完成,这已经是不可逆的了。T2在T1进入write phase之前的读写,肯定和T1的操作不冲突(因为T1 validation通过了)。T2之后继续的读写操作,有可能冲突与T1要提交的操作,因此T2进入validate阶段:

  • 如果WS(T1) ∩ RS(T2)= ∅,说明T2没读到T1写的记录,validation通过,T2可以写入。(为什么不验证WS(T2)了呢?WS(T1)已经提交了,且它的时间戳小于WS(T2),WS(T2)里之前的一部分肯定没有冲突,之后的一部分,因为没有读过T1的写入的对象,写进去也没问题,不会覆盖WS(T1)的写)
  • 否则,T2与T1之间存在读写冲突和写写冲突,T2需要回滚。读写冲突:T2读到了T1写之前的版本,T1提交后,它可能读到T1写的版本,不可重复读。写写冲突:T2有可能在旧版本基础上更新,再次写入,造成T1的更新丢失。

05基于MVCC的并发控制

数据库维护了一条记录的多个物理版本。事务写入时,创建写入数据的新版本,读请求依据事务/语句开始时的快照信息,获取当时已经存在的最新版本数据。它带来的最直接的好处是:写不阻塞读,读也不阻塞写,读请求永远不会因此冲突失败(例如单版本T/O)或者等待(例如单版本2PL)。对数据库请求来说,读请求往往多于写请求。主流的数据库几乎都采用了这项优化技术。

MVCC是读和写请求的优化技术,没有完全解决数据库并发问题,它需要与前述的几种并发控制技术组合,才能提供完整的并发控制能力。常见的并发控制技术种类包括:MV-2PL,MV-T/O和MV-OCC,它们的特点如下表:
image.png
MVCC还有两个关键点需要考虑:多版本数据的存储和多余多版本数据的回收。

多版本数据存储方式,大致可以分为两类:

(1)Append only的方式,新旧版本存储在同一个表空间,例如基于LSM-Tree的存储引擎。

(2)主表空间记录最新版本数据,前镜像记录在其它表空间或数据段,例如InnoDB的多版本信息记录在undo log。多版本数据回收又称为垃圾回收(GC),那些没有机会再被任何读请求获取的旧版本记录,应该被及时删除。

06总结

本文依据冲突处理的时机(乐观程度),依次介绍了基于锁(在事务开始前预防冲突)、基于T/O(在事务执行中判断冲突)和基于Validation(在事务提交时验证冲突)的事务并发控制机制。

不同的实现适用于不同的workload,并发冲突小的workload,当然适合更乐观的并发控制方式。而MVCC可以解决只读事务和读写事务之间相互阻塞的问题,提高了事务的并发读,被大多数主流数据库系统采用。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/516123.shtml

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

相关文章

工作簿长时间空闲时自动关闭

目录 1. 对关闭时间和关闭工作薄进行设置2. 利用 OnTime 方法对执行程序进行设置3. 对工作薄的变化进行设置1. 对关闭时间和关闭工作薄进行设置 在新建模块中声明一个时间作为全局的变量,该变量将是每次工作薄发生改变后延续的时间,另外再设置一个工作薄关闭的过程。 代码如…

打钱!我的数据库被黑客勒索了!

来源 | 小白学黑客责编 | 晋兆雨头图 | 付费下载于视觉中国数据库失陷昨天晚上&#xff0c;读者群里一位小伙伴发消息说自己的数据库被黑了&#xff0c;搞安全的我自然是立刻来了兴趣&#xff0c;加班加点开始分析起来&#xff0c;不知道的还以为我要熬夜等剁手节呢。这位小伙伴…

uni-app集成uview

文章目录一、uni_modules方式1. 创建uni-app项目2. 安装uview3. 安装SCSS插件二、配置步骤2.1. 引入uView主JS库2.2. 引入主题文件2.3. 引入uView基础样式2.4. 配置easycom组件模式2.5. 实战三、zip方式3.1. 创建uni-app项目3.2. 下载uview3.3. 解压重命名3.4. 拷贝UI3.5. 安装…

阿里云飞天洛神2.0:高性能网络软硬一体化技术实践

云网络架构 云计算从9年前被质疑为新瓶装旧酒&#xff0c;到经过多年的高速发展&#xff0c;正在成为水电煤一样的基础设施。云网络构建在物理网络之上&#xff0c;为云计算提供灵动、智能的网络连接。云网络的性能和稳定性是云计算的基石。 VPC是云网络的基础&#xff0c;VP…

新一轮支付革命,利楚扫呗的数据库优化之路

中国移动支付市场崛起过程中&#xff0c;第三方、第四方等非银行支付机构在2017年至2019年之间&#xff0c;把移动支付交易量从300多亿笔拉升至近1800亿笔&#xff0c;充分彰显以扫码支付为代表的移动支付革命引领一时风潮。 利楚扫呗作为华中地区领先的金融收单企业也是其中一…

IM、RTC技术两生花,看融云如何打造“IM+RTC+Push”一站式通信云服务

11月9日&#xff0c;融云宣布完成数亿元人民币的D轮融资。从李佳琦和明星扎堆的电商直播&#xff0c;到疫情期间在线教育的全面普及&#xff0c;再到5G技术进入商用&#xff0c;实时音视频技术&#xff08;Real-Time Communication&#xff0c;以下简称RTC&#xff09;迎来了全…

阿里云助力浙江大学信息化建设,以实时数据驱动校园智能管理

随着网络信息化技术的不断进步&#xff0c;就我国高校而言&#xff0c;传统的学习和生活模式已无法满足学生的需求&#xff1b;老机房堆硬件的建设&#xff0c;给信息化深度建设的管理和运维工作人员造成了巨大的麻烦。这些因素促使各所高校对校园智能管理的需求日益剧增。 20…

1 元秒杀 1000+ 册爆款电子书,错过再等一年!

wow代码人们让钱包瑟瑟发抖的双十一已经来啦与此同时码不停蹄地向你奔赴而来的还有 CSDN 为你准备的???? 1 元秒杀 ????价值 3.5 万元的爆款电子书限时特惠&#xff0c;仅需 1 元你&#xff0c;准备好了吗仅限 1000 人速领????????????错过悔10年系列好书

谊品生鲜:放弃传统数据库架构,全站上阿里云

疫情之下&#xff0c;生鲜电商的订单突然暴涨。 社区生鲜龙头企业谊品生鲜就是一个典型代表。它的在线交易量激增&#xff0c;日订单量超过10万单。 对电商企业而言&#xff0c;这对业务是个巨大的好消息&#xff1b;但对于IT、对于谊品生鲜的核心业务系统则是一个巨大的挑战…

教你一招,如何解决 “这台电脑无法运行 Windows 11” 的问题

目录1. 系统兼容性检测2. 为什么我的电脑不能运行Windows 11&#xff1f;3. 最低系统要求4. 如何开启 TPM 受信任模块5. 微软坚持最低要求的理由6. 最新补充6.1 更新支持的 CPU 列表6.2 旧电脑也能安装 Windows 11&#xff0c;但后果自负微软于 6.24 正式推出了全新的 Windows …

Flink SQL 的 9 个示例

作者&#xff1a;贺小令&#xff08;晓令&#xff09; 本文由阿里巴巴技术专家贺小令分享&#xff0c;社区志愿者郑仲尼整理。文章基于 Flink 1.9 版本&#xff0c;从用户的角度来讲解 Flink 1.9 版本中 SQL 相关原理及部分功能变更&#xff0c;希望对大家有所帮助。主要内容分…

如何破解“中国开源拿来主义”?包云岗的几点分析

作者 | 包云岗责编 | 王晓曼本文经作者授权转载自包云岗知乎以下视频是2011-2019期间全世界开源代码贡献最多的城市排名&#xff1a;旧金山一骑绝尘&#xff0c;其他主要是美欧城市&#xff1b;亚洲有东京、首尔上榜&#xff1b;莫斯科也贡献突出。但是&#xff0c;很遗憾没有中…

uni-app获取腾讯地图计算两经纬度的实际距离(可批量)

地址 https://lbs.qq.com/service/webService/webServiceGuide/webServiceDistance getDistance() {uni.request({url: https://apis.map.qq.com/ws/distance/v1/matrix, //仅为示例&#xff0c;并非真实接口地址。method: GET,data: {mode: walking,from: 39.071510,117.190…

JavaScript 常用工具函数

目录 1. 为元素添加 on 方法2. 为元素添加trigger方法3. 转义 HTML 标签4. HTML 标签转义5. 跨浏览器绑定事件5. 加入收藏夹7. 提取页面代码中所有网址8. 动态加载脚本文件9. 返回顶部的通用方法10. 实现base64解码11. 确认是否是键盘有效输入值12. 全角半角转换13. 版本对比14…

JS前端常用工具方法

目录 1. 邮箱2. 手机号码3. 电话号码4. URL地址5. 是否字符串6. 是否数字7. 是否Boolean8. 是否函数9. 是否为null10. 是否undefined11. 是否对象12. 是否数组13. 是否时间14. 是否正则15. 是否错误对象16. 是否Symbol函数17. 是否Promise对象18. 是否Set对象19. 是否是微信浏览…

JS向数组添加元素,插入数据

js中对于数组的操作很常见&#xff0c;下面记录一下js向数组添加元素的方法。 let myArray[11,22,33];console.log(原数组&#xff1a;,myArray);myArray.push(44,55);console.log(用push在数组后面插入元素&#xff1a;,myArray);myArray.unshift(66,77);console.log(用unshif…

小程序 获取腾讯地图计算两经纬度的实际距离(可批量)_多地打卡

文章目录一、小程序1. 安装vue-jsonp2. 引入腾讯sdk3. 实例化4. 二点求距离5. 多点求距离文档地址&#xff1a;https://lbs.qq.com/service/webService/webServiceGuide/webServiceDistance申请开发者密钥&#xff08;key&#xff09;&#xff1a; 申请密钥一、小程序 1. 安装…

前端常用 JavaScript 方法封装

目录 1. 输入一个值,返回其数据类型**2. 数组去重3. 字符串去重4. 深拷贝 浅拷贝5. reverse底层原理和扩展6. 圣杯模式的继承7. 找出字符串中第一次只出现一次的字母8. 找元素的第 n 级父元素9. 返回元素的第n个兄弟节点10. 封装mychildren,解决浏览器的兼容问题11. 判断元素…

H5 移动端 获取腾讯地图计算两经纬度的实际距离(可批量)_多地打卡

文章目录一、H5移动端1. 安装vue-jsonp2. 引入腾讯sdk3. 实例化4. 二点求距离5. 多点求距离文档地址&#xff1a;https://lbs.qq.com/service/webService/webServiceGuide/webServiceDistance一、H5移动端 1. 安装vue-jsonp npm i -S vue-jsonpmain.js //解决跨域 import {Vu…

JavaScript 中的 3 种 for 循环语句

1. for 应用较广,简单实用。 const array = [4, 7, 9, 2, 6]; for (let index = 0;</