关于分布式锁:全新的分布式锁功能简单且强大

起源:《全新的分布式锁,性能简略且弱小》前言:分布式锁是分布式系统中一个极为重要的工具。目前有多种分布式锁的设计方案,比方借助redis,mq,数据库,zookeeper等第三方服务零碎来设计分布式锁。tldb提供的分布式锁,次要是要简化这个设计的过程,提供一个简洁牢靠,相似应用程序中对象锁的形式来获取分布式锁。tldb提供分布式锁应用办法: lock 阻塞式申请锁trylock 尝试加锁,若锁已被占用,则失败返回,反之,则获取该锁unlock 开释曾经获取的锁 tldb提供的分布式锁性能次要在MQ模块中实现,调用的办法在MQ客户端实现,客户端的实现理论非常简单,除了目前曾经实现的几种语言java,golang,python,javaScript 写的simpleClient,其实其余开发者有趣味也能够实现其余语言的MQ客户端,齐全没有技术门槛。分布式锁由tldb服务器管制,所以它绝对客户端来说,也是跨语言的,如,用java客户端上锁的对象,其余语言同样无奈获取该对象锁。 Lock(string,int) 办法的应用tldb提供的是以字符串为锁对象的独占锁, 如,lock("abc",3) 必须提供两个参数: 第一个参数为锁对象,即服务器对“abc”对象调配一个锁,所有对"abc"对象申请加锁的线程争用一个独占锁,该办法为一个阻塞办法,申请到锁则返回,如果锁被其余线程占用,则始终阻塞直至获取到锁。第二个参数为持有该分布式锁的最长工夫,单位为秒,例如lock("abc",3),意思是,如果超过3秒还没有调用unlock开释该锁,服务器将强制开释该锁,持续将锁调配给其余申请的线程。UnLock(string) 办法的应用UnLock为开释分布式锁时调用的办法。客户端在胜利获取分布式锁后,服务器会返回一个该锁的key,客户端执行完逻辑代码的最初,必须显式调用UnLock(key)来开释该分布式锁。如果没有调用unlock开释锁,tldb将期待锁开释的超时工夫直至超时后强制开释该锁。TryLock(string,int) 办法的应用trylock与lock类似,然而lock办法阻塞的,调用lock办法申请分布式锁时,如果该锁曾经被占用,那么lock办法将始终期待直至tldb服务器将锁调配给它,这与程序中获取独占锁的形式统一。而trylock时非阻塞的,调用trylock后会立刻返回,如果获取到锁,tldb会将标识该锁的key一并返回,如何该锁曾经被占用,服务器将返回空数据。以下以 go为例应用分布式锁 因为tldb分布式的实现是在MQ模块,所以go程序必须应用tlmq-go, tldb的mq客户端进行调用锁办法。 import "github.com/donnie4w/tlmq-go/cli"调用 lock 的程序:lock办法是阻塞的 sc := cli.NewMqClient("ws://127.0.0.1:5001", "mymq=123")sc.Connect()//以上为 客户端连贯MQ服务器key, err := sc.Lock("testlock", 3)//lock中两个参数,第一个参数为字符串,即tldb服务器为“testlock”调配一个全局的分布式锁//第二个参数3为客户端持有该锁的最长工夫,示意超过3秒没有开释锁时,tldb服务器将在服务端强制开释该锁,并调配给其余申请锁的线程if err!=nil{ //获取锁失败,需查看tldb能失常拜访}else{ defer sc.UnLock(key) //获取锁胜利后,必须在程序最初调用Unlock //执行业务逻辑程序} 调用tryLock的程序,trylock是非阻塞的 sc := cli.NewMqClient("ws://127.0.0.1:5001", "mymq=123")sc.Connect() if key, ok := sc.TryLock("testlock2", 3); ok { //ok为true,示意曾经胜利获取到分布式锁 defer sc.UnLock(key) //在程序最初开释锁对象 ... }go用自旋的形式应用trylock获取分布式锁,实现程序的阻塞期待 sc := cli.NewMqClient("ws://127.0.0.1:5001", "mymq=123")sc.Connect()var key stringfor { if v, ok := sc.TryLock("testlock", 3); ok { key = v break } else { <-time.After(100* time.Millisecond) }}defer sc.UnLock(key)...//业务逻辑代码这段程序应该比拟易于了解,就是每隔100毫秒,循环获取字符串“testlock”的分布式锁直至胜利。 ...

September 25, 2023 · 1 min · jiezi

关于分布式锁:写给小白看的分布式锁教程一-基本概念与使用

分布式锁在刚毕业的时候就碰到,然而我过后的兴致倒不是很大,起因在于锁后面被分布式所润饰,一下子就变的高端起来了。到当初的话,我也仅仅是停留在回调办法,没有粗疏的梳理一下相干概念。这次就来疏解一下这概念。本篇要求有Zookeeper、Redis的根底。如果不会能够我掘金的文章: 《Zookeeper学习笔记(一)基本概念和简略应用》 这篇公众号外面有《Redis学习笔记(一) 初遇篇》 这篇还未迁徙到公众号当咱们说起锁时,是在说什么?写本篇的时候我首先想的是事实世界的锁,像上面这样: 事实世界的锁为了爱护资源设计,持有钥匙的人被视为资源的客人,能够获取被锁爱护的资源。在事实世界中的锁大都基于这种设计而生,像门锁避免门外面的资源被偷盗,手机上的指纹锁爱护手机的资源。那软件世界的锁是一种什么样的概念呢?也是为了实现对资源的爱护而生吗? 某种程度上能够这么了解,以多线程下卖票为例,如果不加上锁,那么就会由可能实现两个线程独特卖一张票的状况。所以咱们对获取票,并对总票数进行减去的这个操作加上了synchronized。 public class TicketSell implements Runnable { // 总共一百张票 private int total = 2000; @Override public void run() { while (total > 0) { // total-- 这个操作并非是原子操作, 可能会被打断。 // A线程可能还没拉的及实现减减操作,工夫片耗尽。B线程被进来也读取到了total的值就会呈现 // 两个线程呈现卖一张票的状况 System.out.println(Thread.currentThread().getName() + "正在售卖:" + total--); } }} public static void main(String[] args) { TicketSell ticketSell = new TicketSell(); Thread a = new Thread(ticketSell, "a"); Thread b = new Thread(ticketSell, "b"); a.start(); b.start();}防止这样的状况,咱们能够用乐观锁synchronzed、ReentrantLock锁住代码块来实现防止超卖或者反复买的起因在于咱们开启的两个线程都属于一个JVM过程,total也位于JVM过程内,JVM能够用锁来爱护这个变量。那如果咱们将这个票数挪动到数据库内表里,咱们上的锁还管用吗? 必定是不论用了,起因在于在JVM过程内的归JVM过程管,数据库属于另一个过程,JVM的锁无奈锁另一个过程的变量。 上面咱们将这个总票数挪动到数据库里,重写一下这个卖票的程序, 首先咱们筹备一张表,这里为了图省事,就用我手头外面的Student的最大number来充当总票 , 建表语句如下: ...

October 5, 2022 · 2 min · jiezi

关于分布式锁:分布式锁实现方案

分布式锁是锁的一种,通常用来跟 JVM 锁做区别。JVM 锁就是咱们常说的 synchronized、Lock。JVM 锁只能作用于单个 JVM,能够简略了解为就是单台服务器(容器),而对于多台服务器之间,JVM 锁则没法解决,这时候就须要引入分布式锁。 分布式锁应具备的个性:互斥性:和咱们本地锁一样互斥性是最根本,然而分布式锁须要保障在不同节点的不同线程的互斥。可重入性:同一个节点上的同一个线程如果获取了锁之后那么也能够再次获取这个锁,无需从新竞争锁资源。锁超时:和本地锁一样反对锁超时,避免死锁。反对阻塞和非阻塞:和 ReentrantLock 一样反对 lock 和 trylock 以及 tryLock(long timeOut)。反对偏心锁和非偏心锁(可选):偏心锁的意思是依照申请加锁的程序取得锁,非偏心锁就相同是无序的。这个一般来说实现的比拟少。基于redis分布式锁实现计划Redis 锁次要利用 Redis 的 setnx 命令 加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回胜利,否则返回失败。KEY 是锁的惟一标识,个别按业务来决定命名。解锁命令:DEL key,通过删除键值对开释锁,以便其余线程能够通过 SETNX 命令来获取锁。锁超时:EXPIRE key timeout, 设置 key 的超时工夫,以保障即便锁没有被显式开释,锁也能够在肯定工夫后主动开释,防止资源被永远锁住。则加解锁的伪代码如下: if (setnx(key, 1) == 1){ expire(key, 30) try { //TODO 业务逻辑 } catch (Exception e){ logger.error(e); }finally { del(key) }}上述锁存在一些问题:1:SETNX 和 EXPIRE 非原子性:如果 SETNX 胜利,在设置锁超时工夫后,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时工夫变成死锁。解决方案:Redis 2.6.12 及更高版本中,set命令增加了对”set iff not exist”、”set iff exist”和”expire timeout”语义的反对,即应用setnx命令同时反对设置过期工夫 ...

August 29, 2022 · 3 min · jiezi

关于分布式锁:在分布式系统中库存超卖怎么办

生存中遇到一些的高并发场景,如:618、双11光棍节秒杀流动、节假日时12306火车票抢票等场景,访问量激增,比平时时多了几千或者上万倍的访问量,这些高并发场景会导致库存超卖,库存超卖例子:一趟火车票只有1k张,也就是说这趟火车只能载1k人,1w人在抢该火车的票,大家都抢到票,这样将会有9k人上不了火车,这必定不行的,这不仅仅耽搁客户的行程,也导致了平台的信用度。为了解决分布式系统上库存超卖的状况,产生了分布式锁。 分布式锁是什么呢?分布式锁就是在分布式集群中,实现跨机器共享互斥管制机制,保障操作原子性,保证数据一致性。 上面介绍一套很不错的分布式锁!!! Redislocker 是通过golang语言在redis+ lua根底上实现的一套高可用、高并发的分布式锁.Redislocker 目前实现了mutex个性, 具备以下特点: 分布式:反对多台独立的机器运行排它性,个性跟sync.Mutex相似公平性:遵循先入先出准则性能高:防止羊群效应等,防止羊群效应等,缩小零碎的I/O操作,升高大量cpu、网络等耗费守护协程:避免工作未完结开释锁,为其锁续命避免锁超时:避免宕机后,导致长时间未开释锁Redislocker 应用介绍: 下载 go get -u github.com/nelsonkti/redislocker@latest var ctx = context.Background() var redisClient *redis.Client var session *Session redisClient = redis.NewClient( &redis.Options{ Addr: "0.0.0.0:6379", Password: "", DB: 0, }, ) _, err := redisClient.Ping(ctx).Result() if err != nil { panic(err) } session, err = NewSession(redisClient) locker := RedisLocker(session, key) defer locker.Unlock() locker.Lock()Benchmark 本次测试后果是通过我的低配mac pro 压测, 大家能够参考一下,目前 redislocker 曾经投入公司生产应用,成果挺不错goos: darwingoarch: amd64pkg: Redislockercpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHzBenchmarkLockBenchmarkLock-8 94381 15908 ns/op 5381 B/op 85 allocs/opPASSok Redislocker 2.089shttps://github.com/nelsonkti/redislocker最初,欢送大家点亮小星,多多反对一下,感激 ...

August 11, 2022 · 1 min · jiezi

关于分布式锁:分布式高性能状态与原子操作数据库slock简介

概述我的项目地址:https://github.com/snower/slock 何为状态与原子操作数据库?区别于redis次要用于保留数据,可在多节点多零碎间高效统同步数据,slock则是设计为只用于保留同步状态,简直不能携带数据,高性能的异步二进制协定也保障了在状态达成时高效的被动触发期待零碎。区别于redis被动查看的过期工夫,slock的期待超时工夫及锁定过期工夫都是准确被动触发的。多核反对、更简略的系统结构也保障了其领有远超redis的性能及延时,这也更合乎状态同步需要中更高性能更低延时的需要。 秒杀为何难做?其问题就是咱们须要在很短时间内实现大量的有效申请中夹杂仅很少的无效申请解决,进一步简化就是须要实现超高并发下海量申请间的状态同步的过程,slock高QPS能够疾速解决过滤大量有效申请的问题,高性能的原子操作又能够很好的解决抢库存的逻辑。 随着nodejs的应用,异步IO相干框架也越来越成熟,应用也越来越不便,多线程同步IO模式下,某些场景很多时候咱们须要转化为队列解决而后再推送后果,但异步IO就齐全不须要这么简单,间接加分布式锁期待可用就行,整个过程齐全回到了单机多线程编程的逻辑,更简略也更容易了解和保护了,比方下单申请须要操作很多,在高并发下可能须要发送到队列中解决实现再推送后果,但用异步IO的加分布式锁话,认真看异步IO加锁其实又组成了一个更大的分布式队列,大大简化了实现步骤。 个性超高性能,在Intel i5-4590上超过200万QPS高性能二进制异步协定简略稳固牢靠,也可用redis同步文本协定多核多线程反对4级AOF长久化 不长久化间接返回超过过期工夫百分比工夫后长久化后返回超过AOF工夫长久化后返回立即异步长久化后返回需整个集群沉闷节点都胜利并长久化后返回高可用集群模式,主动迁徙、主动代理准确到毫秒、秒、分钟超时、过期工夫,可独自订阅超时、过期工夫屡次锁定反对,重入锁定反对遗嘱命令场景示例分布式锁整个协定只有中间指令,Lock和Unlock,分布式锁也即是最罕用场景,和redis实现分布式锁区别除了性能更好延时也更低外,期待超时及锁定超时过期工夫时准确被动触发的,所以有wait机制,redis实现的分布式锁个别则须要client被动延时重试来查看。 package main;import io.github.snower.jaslock.Client;import io.github.snower.jaslock.Event;import io.github.snower.jaslock.Lock;import io.github.snower.jaslock.ReplsetClient;import io.github.snower.jaslock.exceptions.SlockException;import java.io.IOException;import java.nio.charset.StandardCharsets;public class App { public static void main(String[] args) { ReplsetClient replsetClient = new ReplsetClient(new String[]{"172.27.214.150:5658"}); try { replsetClient.open(); Lock lock = replsetClient.newLock("test".getBytes(StandardCharsets.UTF_8), 5, 5); lock.acquire(); lock.release(); } catch (SlockException e) { e.printStackTrace(); } finally { replsetClient.close(); } }}nginx & openresty限流openresty应用此服务实现限流能够很不便的实现跨节点,同时因为应用高性能异步二进制协定,每个work只须要一个和server的连贯,高并发下不会产生外部连贯耗尽的问题,server主节点变更的时候work可主动应用新可用主节点,实现高可用。 最大并发数限流每个key能够设置最大锁定次数,应用该逻辑能够十分不便的实现最大并发限流。 lua_package_path "lib/resty/slock.lua;";init_worker_by_lua_block { local slock = require("slock") slock:connect("lock1", "127.0.0.1", 5658)}server { listen 80; location /flow/maxconcurrent { access_by_lua_block { local slock = require("slock") local client = slock:get("lock1") local flow_key = "flow:maxconcurrent" local args = ngx.req.get_uri_args() for key, val in pairs(args) do if key == "flow_key" then flow_key = val end end local lock = client:newMaxConcurrentFlow(flow_key, 10, 5, 60) local ok, err = lock:acquire() if not ok then ngx.say("acquire error:" .. err) ngx.exit(ngx.HTTP_OK) else ngx.ctx.lock1 = lock end } echo "hello world"; log_by_lua_block { local lock = ngx.ctx.lock1 if lock ~= nil then local ok, err = lock:release() if not ok then ngx.log(ngx.ERR, "slock release error:" .. err) end end } }}令牌桶限流每个key能够设置最大锁定次数,并设置为在令牌到期时过期,即可实现令牌桶限流,应用毫秒级过期工夫的时候也能够从此形式来实现削峰均衡流量。 ...

December 20, 2021 · 2 min · jiezi

关于分布式锁:灵活运用分布式锁解决数据重复插入问题

一、业务背景许多面向用户的互联网业务都会在零碎后端保护一份用户数据,快利用核心业务也同样做了这件事。快利用核心容许用户对快利用进行珍藏,并在服务端记录了用户的珍藏列表,通过用户账号标识OpenID来关联珍藏的快利用包名。 为了使用户在快利用核心的珍藏列表可能与快利用Menubar的珍藏状态买通,咱们同时也记录了用户账号标识OpenID与客户端本地标识local\_identifier的绑定关系。因为快利用Manubar由快利用引擎持有,独立于快利用核心外,无奈通过账号体系获取到用户账号标识,只能获取到客户端本地标识local\_identifier,所以咱们只能通过二者的映射关系来放弃状态同步。 在具体实现上,咱们是在用户启动快利用核心的时候触发一次同步操作,由客户端将OpenID和客户端本地标识提交到服务端进行绑定。服务端的绑定逻辑是:判断OpenID是否曾经存在,如果不存在则插入数据库,否则更新对应数据行的local\_identifier字段(因为用户可能先后在两个不同的手机上登录同一个vivo账号)。在后续的业务流程中,咱们就能够依据OpenID查问对应的local\_identifier,反之亦可。 然而代码上线一段时间后,咱们发现t_account数据表中竟然存在许多反复的OpenID记录。依据如上所述的绑定逻辑,这种状况实践上是不应该产生的。所幸这些反复数据并没有对更新和查问的场景造成影响,因为在查问的SQL中咱们退出了LIMIT 1的限度,因而针对一个OpenID的更新和查问操作实际上都只作用于ID最小的那条记录。 二、问题剖析与定位尽管冗余数据没有对理论业务造成影响,然而这种显著的数据问题也必定是不能容忍的。因而咱们开始着手排查问题。 首先想到的就是从数据自身动手。先通过对t_account表数据进行粗略察看,发现大概有3%的OpenID会存在反复的状况。也就是说反复插入的状况是偶现的,大多数申请的解决都是依照预期被正确处理了。咱们对代码从新进行了走读,确认了代码在实现上的确不存在什么显著的逻辑谬误。 咱们进一步对数据进行粗疏察看。咱们筛选了几个呈现反复状况的OpenID,将相干的数据记录查问进去,发现这些OpenID反复的次数也不尽相同,有的只反复一次,有的则更多。然而,这时候咱们发现了一个更有价值的信息——这些雷同OpenID的数据行的创立工夫都是完全相同的,而且自增ID是间断的。 于是,咱们猜想问题的产生应该是因为并发申请造成的!咱们模仿了客户端对接口的并发调用,的确呈现了反复插入数据的景象,进一步证实了这个猜想的合理性。然而,明明客户端的逻辑是每个用户在启动的时候进行一次同步,为什么会呈现同一个OpenID并发申请呢? 事实上,代码的理论运行并不如咱们设想中的那么现实,计算机的运行过程中往往存在一些不稳固的因素,比方网络环境、服务器的负载状况。而这些不稳固因素就可能导致客户端发送申请失败,这里的“失败”可能并不意味着真正的失败,而是可能整个申请工夫过长,超过了客户端设定的超时工夫,从而被人为地断定为失败,于是通过重试机制再次发送申请。那么最终就可能导致同样的申请被提交了屡次,而且这些申请兴许在两头某个环节被阻塞了(比方当服务器的解决线程负载过大,来不及解决申请,申请进入了缓冲队列),当阻塞缓解后这几个申请就可能在很短的工夫内被并发解决了。 这其实是一个典型的并发抵触问题,能够把这个问题简略形象为:如何防止并发状况下写入反复数据。事实上,有很多常见的业务场景都可能面临这个问题,比方用户注册时不容许应用雷同的用户名。 一般来说,咱们在解决这类问题时,最直观的形式就是先进行一次查问,当判断数据库中不存在以后数据时才容许插入。 显然,这个流程从单个申请的角度来看是没有问题的。然而当多个申请并发时,申请A和申请B都先发动一次查问,并且都失去后果是不存在,于是两者都又执行了数据插入,最终导致并发抵触。 三、摸索可行的计划既然问题定位到了,接下来就要开始寻求解决方案了。面对这种状况,咱们通常有两种抉择,一种是让数据库来解决,另一种是由应用程序来解决。 3.1 数据库层面解决——惟一索引当应用MySQL数据库及InnoDB存储引擎时,咱们能够利用惟一索引来保障同一个列的值具备唯一性。显然,在t\_account这张表中,咱们最开始是没有为open\_id列创立惟一索引的。如果咱们想要此时加上惟一索引的话,能够利用下列的ALTER TABLE语句。 ALTER TABLE t_account ADD UNIQUE uk_open_id( open_id );一旦为open\_id列加上惟一索引后,当上述并发状况产生时,申请A和申请B中必然有一者会优先实现数据的插入操作,而另一者则会失去相似谬误。因而,最终保障t\_account表中只有一条openid=xxx的记录存在。 Error Code: 1062. Duplicate entry 'xxx' for key 'uk_open_id'3.2 应用程序层面解决——分布式锁另一种解决的思路是咱们不依赖底层的数据库来为咱们提供唯一性的保障,而是靠应用程序本身的代码逻辑来防止并发抵触。应用层的保障其实是一种更具通用性的计划,毕竟咱们不能假如所有零碎应用的数据长久化组件都具备数据唯一性检测的能力。 那具体怎么做呢?简略来说,就是化并行为串行。之所以咱们会遇到反复插入数据的问题,是因为“检测数据是否曾经存在”和“插入数据”两个动作被宰割开来。因为这两个步骤不具备原子性,才导致两个不同的申请能够同时通过第一步的检测。如果咱们可能把这两个动作合并为一个原子操作,就能够防止数据抵触了。这时候咱们就须要通过加锁,来实现这个代码块的原子性。 对于Java语言,大家最相熟的锁机制就是synchronized关键字了。 public synchronized void submit(String openId, String localIdentifier){ Account account = accountDao.find(openId); if (account == null) { // insert } else { // update }}然而,事件可没这么简略。要晓得,咱们的程序可不是只部署在一台服务器上,而是部署了多个节点。也就是说这里的并发不仅仅是线程间的并发,而是过程间的并发。因而,咱们无奈通过java语言层面的锁机制来解决这个同步问题,咱们这里须要的应该是分布式锁。 3.3 两种解决方案的衡量基于以上的剖析,看上去两种计划都是可行的,但最终咱们抉择了分布式锁的计划。为什么明明第一种计划只须要简略地加个索引,咱们却不采纳呢? ...

July 26, 2021 · 2 min · jiezi

关于分布式锁:还不会使用分布式锁教你三种分布式锁实现的方式

摘要:在单过程的零碎中,当存在多个线程能够同时扭转某个变量时,就须要对变量或代码块做同步,使其在批改这种变量时可能线性执行打消并发批改变量,而同步实质上通过锁来实现。本文分享自华为云社区《还不会应用分布式锁?从零开始基于 etcd 实现分布式锁》,原文作者:aoho 。 为什么须要分布式锁?在单过程的零碎中,当存在多个线程能够同时扭转某个变量时,就须要对变量或代码块做同步,使其在批改这种变量时可能线性执行打消并发批改变量。而同步实质上通过锁来实现。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么须要在某个中央做个标记,这个标记必须每个线程都能看到,当标记不存在时能够设置该标记,其余后续线程发现曾经有标记了则期待领有标记的线程完结同步代码块勾销标记后再去尝试设置标记。 而在分布式环境下,数据一致性问题始终是难点。相比于单过程,分布式环境的状况更加简单。分布式与单机环境最大的不同在于其不是多线程而是多过程。多线程因为能够共享堆内存,因而能够简略的采取内存作为标记存储地位。而过程之间甚至可能都不在同一台物理机上,因而须要将标记存储在一个所有过程都能看到的中央。 常见的是秒杀场景,订单服务部署了多个服务实例。如秒杀商品有 4 个,第一个用户购买 3 个,第二个用户购买 2 个,现实状态下第一个用户能购买胜利,第二个用户提醒购买失败,反之亦可。而理论可能呈现的状况是,两个用户都失去库存为 4,第一个用户买到了 3 个,更新库存之前,第二个用户下了 2 个商品的订单,更新库存为 2,导致业务逻辑出错。 在下面的场景中,商品的库存是共享变量,面对高并发情景,须要保障对资源的拜访互斥。在单机环境中,比方 Java 语言中其实提供了很多并发解决相干的 API,然而这些 API 在分布式场景中就无能为力了,因为分布式系统具备多线程和多过程的特点,且散布在不同机器中,synchronized 和 lock 关键字将失去原有锁的成果,。仅依赖这些语言本身提供的 API 并不能实现分布式锁的性能,因而须要咱们想想其它办法实现分布式锁。 常见的锁计划如下: 基于数据库实现分布式锁基于 Zookeeper 实现分布式锁基于缓存实现分布式锁,如 redis、etcd 等上面咱们简略介绍下这几种锁的实现,并重点介绍 etcd 实现锁的办法。 基于数据库的锁基于数据库的锁实现也有两种形式,一是基于数据库表,另一种是基于数据库的排他锁。 基于数据库表的增删基于数据库表增删是最简略的形式,首先创立一张锁的表次要蕴含下列字段:办法名,工夫戳等字段。 具体应用的办法为:当须要锁住某个办法时,往该表中插入一条相干的记录。须要留神的是,办法名有唯一性束缚。如果有多个申请同时提交到数据库的话,数据库会保障只有一个操作能够胜利,那么咱们就能够认为操作胜利的那个线程取得了该办法的锁,能够执行办法体内容。执行结束,须要删除该记录。 对于上述计划能够进行优化,如利用主从数据库,数据之间双向同步。一旦主库挂掉,将应用服务疾速切换到从库上。除此之外还能够记录以后取得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果以后机器的主机信息和线程信息在数据库能够查到的话,间接把锁调配给该线程,实现可重入锁。 基于数据库排他锁咱们还能够通过数据库的排他锁来实现分布式锁。基于 Mysql 的 InnoDB 引擎,能够应用以下办法来实现加锁操作: public void lock(){ connection.setAutoCommit(false) int count = 0; while(count < 4){ try{ select * from lock where lock_name=xxx for update; if(后果不为空){ //代表获取到锁 return; } }catch(Exception e){ } //为空或者抛异样的话都示意没有获取到锁 sleep(1000); count++; } throw new LockException();}在查问语句前面减少 for update,数据库会在查问过程中给数据库表减少排他锁。当某条记录被加上排他锁之后,其余线程无奈再在该行记录上减少排他锁。其余没有获取到锁的就会阻塞在上述 select 语句上,可能的后果有 2 种,在超时之前获取到了锁,在超时之前仍未获取到锁。 ...

May 18, 2021 · 2 min · jiezi

关于分布式锁:分布式锁的演化分布式锁居然还能用MySQL

前言之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的应用形式,然而当初很多利用零碎都是相当宏大的,很多利用零碎都是微服务的架构体系,那么在这种跨jvm的场景下,咱们又该如何去解决并发。 单体利用锁的局限性在进入实战之前简略和大家粗略聊一下互联网零碎中的架构演进。 在互联网零碎倒退之初,耗费资源比拟小,用户量也比拟小,咱们只部署一个tomcat利用就能够满足需要。一个tomcat咱们能够看做是一个jvm的过程,当大量的申请并发达到零碎时,所有的申请都落在这惟一的一个tomcat上,如果某些申请办法是须要加锁的,比方上篇文章中提及的秒杀扣减库存的场景,是能够满足需要的。然而随着访问量的减少,一个tomcat难以撑持,这时候咱们就须要集群部署tomcat,应用多个tomcat撑持起零碎。 在上图中简略演变之后,咱们部署两个Tomcat独特撑持零碎。当一个申请达到零碎的时候,首先会通过nginx,由nginx作为负载平衡,它会依据本人的负载平衡配置策略将申请转发到其中的一个tomcat上。当大量的申请并发拜访的时候,两个tomcat独特承当所有的访问量。这之后咱们同样进行秒杀扣减库存的时候,应用单体利用锁,还能满足需要么? 之前咱们所加的锁是JDK提供的锁,这种锁在单个jvm下起作用,当存在两个或者多个的时候,大量并发申请扩散到不同tomcat,在每个tomcat中都能够避免并发的产生,然而多个tomcat之间,每个Tomcat中取得锁这个申请,又产生了并发。从而扣减库存的问题仍旧存在。这就是单体利用锁的局限性。那咱们如果解决这个问题呢?接下来就要和大家分享分布式锁了。 分布式锁什么是分布式锁?那么什么是分布式锁呢,在说分布式锁之前咱们看到单体利用锁的特点就是在一个jvm进行无效,然而无奈逾越jvm以及过程。所以咱们就能够下一个不那么官网的定义,分布式锁就是能够逾越多个jvm,逾越多个过程的锁,像这样的锁就是分布式锁。 设计思路 因为tomcat是java启动的,所以每个tomcat能够看成一个jvm,jvm外部的锁无奈逾越多个过程。所以咱们实现分布式锁,只能在这些jvm外去寻找,通过其余的组件来实现分布式锁。 上图两个tomcat通过第三方的组件实现跨jvm,跨过程的分布式锁。这就是分布式锁的解决思路。 实现形式那么目前有哪些第三方组件来实现呢?目前比拟风行的有以下几种: 数据库,通过数据库能够实现分布式锁,然而高并发的状况下对数据库的压力比拟大,所以很少应用。Redis,借助redis能够实现分布式锁,而且redis的java客户端品种很多,所以应用办法也不尽相同。Zookeeper,也能够实现分布式锁,同样zk也有很多java客户端,应用办法也不同。针对上述实现形式,老猫还是通过具体的代码例子来一一演示。 基于数据库的分布式锁思路:基于数据库乐观锁去实现分布式锁,用的次要是select ... for update。select ... for update是为了在查问的时候就对查问到的数据进行了加锁解决。当用户进行这种行为操作的时候,其余线程是禁止对这些数据进行批改或者删除操作,必须期待上个线程操作结束开释之后能力进行操作,从而达到了锁的成果。 实现:咱们还是基于电商中超卖的例子和大家分享代码。 咱们还是利用上次单体架构中的超卖的例子和大家分享,针对上次的代码进行革新,咱们新键一张表,叫做distribute_lock,这张表的目标次要是为了提供数据库锁,咱们来看一下这张表的状况。因为咱们这边模仿的是订单超卖的场景,所以在上图中咱们有一条订单的锁数据。 咱们将上一篇中的代码革新一下抽取出一个controller而后通过postman去申请调用,当然后盾是启动两个jvm进行操作,别离是8080端口以及8081端口。实现之后的代码如下: /** * @author kdaddy@163.com * @date 2021/1/3 10:48 * @desc 公众号“程序员老猫” */@Service@Slf4jpublic class MySQLOrderService { @Resource private KdOrderMapper orderMapper; @Resource private KdOrderItemMapper orderItemMapper; @Resource private KdProductMapper productMapper; @Resource private DistributeLockMapper distributeLockMapper; //购买商品id private int purchaseProductId = 100100; //购买商品数量 private int purchaseProductNum = 1; @Transactional(propagation = Propagation.REQUIRED) public Integer createOrder() throws Exception{ log.info("进入了办法"); DistributeLock lock = distributeLockMapper.selectDistributeLock("order"); if(lock == null) throw new Exception("该业务分布式锁未配置"); log.info("拿到了锁"); //此处为了手动演示并发,所以咱们临时在这里休眠1分钟 Thread.sleep(60000); KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId); if (product==null){ throw new Exception("购买商品:"+purchaseProductId+"不存在"); } //商品以后库存 Integer currentCount = product.getCount(); log.info(Thread.currentThread().getName()+"库存数"+currentCount); //校验库存 if (purchaseProductNum > currentCount){ throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无奈购买"); } //在数据库中实现减量操作 productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId()); //生成订单 ...次数省略,源代码能够到老猫的github下载:https://github.com/maoba/kd-distribute return order.getId(); }}SQL的写法如下: ...

January 3, 2021 · 2 min · jiezi

关于分布式锁:夜深人静了我们来学一下分布式锁

记录一下明天的文章开始写的工夫00:53,夜深人静了,咱们来学一下分布式锁,咱们要悄悄地学习,而后教训所有人。什么是分布式锁?分布式锁又能够解决哪些问题呢?在咱们的零碎还没有应用分布式架构的时候,咱们能够用同步锁或者Lock锁,来保障多线程并发的时候,同一时间只有一个线程批改共享变量或者执行代码块,然而当咱们当初大部分零碎都是分布式集群部署的,单纯的同步锁和Lock锁只能保障单个实例上的数据一致性,多实例就失去了作用。 这个时候就须要应用分布式锁来保障共享资源的原子性,比方咱们电商零碎外面的扣减库存,当单量小的时候问题不大,如果单量很大,同一时间多个实例都在并发解决扣减库存的业务的时候,就可能存在超卖的问题。 分布式锁的实现?常见的分布式锁有数据库实现分布式锁、Zookeeper实现分布式锁、Redis实现分布式锁、Redisson实现。其中数据库实现分布式锁比较简单,也很容易了解,间接基于数据库实现就能够了,在一些分布式的业务中也常常应用,然而这种形式也是效率最低的,个别是不应用的,咱们就着重介绍一下其余三种形式的实现。 Zookeeper实现分布式锁应用Zookeeper来实现分布式锁就比拟常见,比方很多我的项目就应用Zookeeper作为分布式注册核心,就喜爱用Zookeeper来实现分布式锁,这次要是借助于Zookeeper的两大个性:程序长期节点、Watch机制。 程序长期节点:相熟Zookeeper的同学都晓得,Zookeeper提供了多层级的节点命名空间,每个节点都是用斜杠分隔的门路来示意,相似于咱们的文件夹。节点又分为长久节点和长期节点,节点还能够标记为有序,当节点被标记为有序性,这个节点就具备程序自增的特点,咱们就能够借助这个特点来创立咱们所需的节点。 Watch机制:Watch机制是Zookeeper另一个重要的个性,咱们能够在指定节点上注册一些Watcher,在一些特定的事件触发的时候,告诉用户这个事件。 Zookeeper实现分布式锁的过程咱们先创立一个长久节点作为父节点,每当须要拜访创立分布式锁的时候,就在这个父节点下创立相应的长期的程序子节点,以长期节点名称、父节点名称和顺序号组成特点的名称。在建设子节点后,对父节点下以这个这个子节点名称结尾的子节点进行排序,判断刚建设的节点顺序号是不是最小的,如果是最小的则获取锁,如果不是最小节点,则阻塞期待锁,并且在获取该节点的上一程序节点注册Watcher,期待节点对应的操作取得锁。 当业务解决完之后,删除该节点,敞开zk,进而触发Watcher,开释该锁。 上图就是就是严格依照程序拜访的分布式锁实现,更多的时候咱们引入一些框架来帮忙咱们实现,比方最罕用的Curator框架,代码如下: InterProcessMutex lock = new InterProcessMutex(client, lockPath);if ( lock.acquire(maxWait, waitUnit) ) { try { // 业务解决 } finally{ lock.release(); }}Zookeeper来实现分布式锁人造的劣势就是,Zookeeper是集群实现的,咱们生产环境个别也是集群部署的,能够防止单点问题,稳定性较好,能保障每次操作都能够开释锁。 毛病就是,频繁的创立删除节点,加上注册watch事件,对于zookeeper集群的压力比拟大,性能这一块也比不上Redis实现的分布式锁。 Redis实现分布式锁Redis实现的分布式锁,最为简单,然而性能确是最佳的,所以在对性能要求更高的零碎里,咱们都抉择应用Redis来实现分布式锁。利用Redis实现分布式锁,个别都是应用SETNX实现,举个简略的例子: public static boolean getDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if ("OK".equals(result)) { return true; } return false;}SETNX办法保障设置锁和锁过期工夫的原子性,然而对于锁的过期工夫设置咱们要留神,如果执行业务 工夫比拟长,咱们设置的过期工夫又比拟短的状况下就会造成,业务还没执行完,锁已开释的问题。所以咱们须要依据理论业务解决来评估设置锁的过期工夫,来保障业务能够失常的解决完。 Redisson实现分布式锁Redisson是架设在Redis根底上的一个Java驻内存数据网格。Redisson在基于NIO的Netty框架上,充沛的利用了Redis键值数据库提供的一系列劣势,在Java实用工具包中罕用接口的根底上,为使用者提供了一系列具备分布式个性的常用工具类。性能也比咱们罕用的jedis好一些。 Redisson不论是单节点模式还是集群模式,都很好的实现了分布式锁,个别用的多的都是集群模式,在集群模式下,Redisson应用RedLock算法,很好的解决了Master节点宕机时切换到另外一个Master节点过程中多个利用取得锁。 Redisson集群模式获取锁的实现就是,在不同节点上获取锁,每个节点上获取锁都有超时工夫,如果获取锁超时就认为这个节点不可用,当胜利获取锁的个数超过Redis节点的半数,且获取锁耗费的工夫还没超过锁过期工夫,则认为获取锁胜利。获取锁胜利后从新计算锁开释工夫,由原来的锁开释工夫减去获取锁耗费的工夫,如果最终获取锁失败,曾经获取锁胜利的节点也会开释锁。 具体的代码实现: 引入依赖 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.1</version></dependency>Redisson配置文件: ...

December 9, 2020 · 1 min · jiezi

关于分布式锁:分布式锁

在jvm中,咱们能够通过synchronized或者cas的lock加锁。又有单机的性能太差,无奈适应高并发的需要,所以咱们做了集群,此时jvm是无法控制其余jvm的锁的,这个时候只能分布式锁解决。分布式锁的实质,就是互斥,被A霸占了资源,BCDEF等都不能用,把并行的操作,转为串行。 数据库主键或惟一索引咱们晓得,主键和惟一索引是不能反复的,所以咱们能够利用这个做到资源的互斥。通过insert语句,insert into table VALUES(id,col1,col2),如果插入胜利,阐明拿到了锁,如果插入失败,报Duplicate entry...key 'PRIMARY'的谬误,阐明锁曾经被拿到了。拿到锁的利用,操作实现,只须要删除这个id就能够开释锁了,其余利用就能够insert胜利拿到锁。因为insert和delete是两个操作,如果delete失败,则锁无奈开释。所以插入的时候还要记录插入的工夫,而后跑一个定时工作,看他曾经多久没开释锁了,如果工夫超过设定的阈值,则阐明他删除失败,把这个记录删除开释锁。这边又引入了一个问题,阈值要怎么设置?设置长了,会导致其余利用长时间获取不到锁,设置短了,获取锁的利用还没执行完,锁就生效了,而后其余利用由获取到这个锁,达不到对资源互斥的成果。 乐观锁次要是通过version来判断。update table set col1=val,version=v1 where version=v2,先从数据库取出version,再执行下面的语句,如果执行胜利,阐明拿到锁。乐观锁和主键的长处是他不须要开释锁。然而在大量的并发下,频繁的操作这个表,可能会导致数据库的不可用。 乐观锁借助mysql数据库的FOR UPDATE,FOR UPDATE是一种行级锁,又叫排它锁。FOR UPDATE仅实用于InnoDB,且必须在事务处理模块(BEGIN/COMMIT)中能力失效。用法如下: #开始事务begin;#乐观锁select col1 from table where id=1 for update;#解决其余业务#...#提交事务commit;乐观锁尽管保障了串行化,然而每次申请都会申请锁,很容易因为大量的申请导致数据库的不可用。 redisredis - 分布式锁 zookeeperzookeeper之分布式锁 etcd比照从性能上来说,redis>etcd>zookeeper>数据库,从cap模型来说,redis是ap模型,zookeeper、etcd是cp模型,数据库是ac模型(做主从就是ap)。从业务来说,比方对钱的操作,须要强一致性的,那要用cp模型的分布式锁,比方zookeeper,etcd。如果不须要强一致性的,容许偶然的数据问题,那能够用redis。数据只适宜在并发比拟小的状况下用。

November 12, 2020 · 1 min · jiezi

关于分布式锁:分布式锁的几种实现

一、分布式锁概述在分布式系统架构下,资源共享不再是单机下的线程竞争,而是跨JVM过程之间的而资源共享,因而JVM锁不再满足业务需要,须要引入适宜分布式系统的“锁”。 二、分布式锁设计准则排他性:被共享资源批准工夫内只能被一台机器上的一个线程应用,这点和jvm锁是一个情理。防止死锁:线程获取到锁,在执行完业务之后,肯定要开释锁(包含异常情况下开释)。高可用:获取和开释锁要保障高可用和性能。可重入:最好是可重入锁,即以后机器的以后线程如果没有获取到锁,那么在期待肯定工夫后肯定要保障能够再被获取到。偏心锁:不是硬性要求,指的是不同线程获取锁时最好保障几率一样。三、分布式锁实现形式1. 基于数据库级别的锁乐观锁:基于CAS(compare and swap)原理,即在表中增加一个version字段,每次更新的时候以version作为条件,只能有一个线程更新胜利。乐观锁:总是假如事件产生在最坏的状况,因而每次获取数据时都会上锁,阻塞其余线程,比方行锁、表锁、读锁、写锁。mysql和oracle是通过for update来实现的select fields from table for update 2. 基于redis的原子操作次要通过Redis原子操作SETNX和EXPIRE来实现,因为redis是单线程机制,所以同一时刻,同一节点只容许一个线程执行某种操作,所以满足原子性。 结构一个与共享资源相干的key调用SETNX命令获取锁,并且设置过期工夫,以防死锁开释锁3. 基于zookeeper的互斥排他锁zookeeper分布式锁基于zk程序长期节点和watcher机制,举荐应用框架curator。 原理:在每一个节点上面创立子节点的时候,抉择EPHEMERAL_SEQUENTIAL或者PERSISTENT_SEQUENTIAL。新的子节点前面,会加上一个秩序编号。这个秩序编号,是上一个生成的秩序编号加一。 所以,能够规定编号最小的那个节点取得锁,其余节点只有监听本人前一个节点(通过订阅比本人小的节点的删除事件),并判断本人是不是最小的那个节点就能够了。 步骤: 创立一个根节点,最好是长久节点,代表分布式锁须要占用锁的时候,在根节点下创立长期有序节点。判断本人是否为以后节点列表中最小子节点,如果是则取得锁,否则监听前一个子节点的变更音讯。解决业务流程,解决实现后,删除本人的子节点,开释锁。

October 15, 2020 · 1 min · jiezi

关于分布式锁:基于redis实现分布式锁

背景基于redis实现。 代码package xxx.trade.util;import xxx.core.exception.BizException;import org.apache.commons.lang.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import redis.clients.jedis.JedisCluster;import java.util.Collections;public class JedisUtil { private static final Logger LOGGER = LoggerFactory .getLogger(JedisUtil.class); private static JedisCluster jedisCluster; private static final String PREFIX="xxx-callback:"; public JedisUtil() { // do nothing } public static boolean lock(String key , String value , String nxx, Long lockExpireTimeOut) { if (StringUtils.isBlank(key)) { throw new BizException("key must not null!"); } else { LOGGER.info("JedisTemplate:get cache key={},value={}", key, value); String result = jedisCluster.set(PREFIX+key ,value,nxx,"EX",lockExpireTimeOut); if ("OK".equals(result)) { return true; } return false; } } public static boolean unlock(String key, String value) { try { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedisCluster.eval(luaScript, Collections.singletonList(PREFIX + key), Collections.singletonList(value)); if (!"0".equals(result.toString())) { return true; } } catch (Exception ex) { LOGGER.error("unlock error"); } return false; } static { ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"conf/springconf/redis/redis-spring-context.xml"}); jedisCluster = (JedisCluster)context.getBean("jedisClusterConfigA"); }}应用次要是两步 1.获取锁 2.开释锁 ...

October 5, 2020 · 1 min · jiezi

关于分布式锁:redis分布式锁以及会出现的问题

一、redis实现分布式锁的次要原理:1.加锁最简略的办法是应用setnx命令。key是锁的惟一标识,按业务来决定命名。比方想要给一种商品的秒杀流动加锁,能够给key命名为 “lock_sale_商品ID” 。而value设置成什么呢?咱们能够权且设置成1。加锁的伪代码如下: setnx(key,1)当一个线程执行setnx返回1,阐明key本来不存在,该线程胜利失去了锁;当一个线程执行setnx返回0,阐明key曾经存在,该线程抢锁失败。2.解锁 有加锁就得有解锁。当失去锁的线程执行完工作,须要开释锁,以便其余线程能够进入。开释锁的最简略形式是执行del指令,伪代码如下:del(key)开释锁之后,其余线程就能够继续执行setnx命令来取得锁。3.锁超时锁超时是什么意思呢?如果一个失去锁的线程在执行工作的过程中挂掉,来不及显式地开释锁,这块资源将会永远被锁住,别的线程再也别想进来。所以,setnx的key必须设置一个超时工夫,以保障即便没有被显式开释,这把锁也要在肯定工夫后主动开释。setnx不反对超时参数,所以须要额定的指令,伪代码如下:expire(key, 30) 二、加锁的代码/** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 申请标识 * @param expireTime 超期工夫 * @return 是否获取胜利 */public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 若在这里程序忽然解体,则无奈设置过期工夫,将产生死锁 jedis.expire(lockKey, expireTime); }}下面的代码有一个致命的问题,就是加锁和设置过期工夫不是原子操作。那么会有两种极其状况:一种是在并发状况下,两个线程同时执行setnx,那么失去的后果都是1,这样两个线程同时拿到了锁。别一种是如代码正文所示,即执行完setnx,程序解体没有执行过期工夫,那这把锁就永远不会被开释,造成了死锁。之所以有人这样实现,是因为低版本的jedis并不反对多参数的set()办法。正确的代码如下: /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 申请标识 * @param expireTime 超期工夫 * @return 是否获取胜利 */public static boolean tryGetDistributedLock(Jedis jedis,String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { return true; } return false;}这个set()办法一共有五个形参: ...

September 2, 2020 · 2 min · jiezi

分布式锁4基于Mysql实现

分布式锁(4)-基于Mysql实现1.使用场景在分布式系统里,我们有时执行定时任务,或者处理某些并发请求,需要确保多点系统里同时只有一个执行线程进行处理。 分布式锁就是在分布式系统里互斥访问资源的解决方案。 通常我们会更多地使用Redis分布式锁、Zookeeper分布式锁的解决方案。 本篇文章介绍的是基于MySQL实现的分布式锁方案,性能上肯定是不如Redis、Zookeeper。 所以,基于Mysql实现分布式锁,适用于对性能要求不高,并且不希望因为要使用分布式锁而引入新组件。 2.基于唯一索引(insert)实现2.1 实现方式获取锁时在数据库中insert一条数据,包括id、方法名(唯一索引)、线程名(用于重入)、重入计数获取锁如果成功则返回true获取锁的动作放在while循环中,周期性尝试获取锁直到结束或者可以定义方法来限定时间内获取锁释放锁的时候,delete对应的数据2.2 优点:实现简单、易于理解2.3 缺点没有线程唤醒,获取失败就被丢掉了;没有超时保护,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁;这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用;并发量大的时候请求量大,获取锁的间隔,如果较小会给系统和数据库造成压力;这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作;这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁,因为数据中数据已经存在了;这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。2.4 简单实现方案新建一张表,用于存储锁的信息,需要加锁的时候就插入一条记录,释放锁的时候就删除这条记录 新建一张最简单的表 CREATE TABLE `t_lock` ( `lock_key` varchar(64) NOT NULL COMMENT '锁的标识', PRIMARY KEY (`lock_key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁'根据插入sql返回受影响的行数,大于0表示成功占有锁 insert ignore into t_lock(lock_key) values(:lockKey)释放锁的时候就删除记录 delete from t_lock where lock_key = :lockKey2.5 完善实现方案上面这种简单的实现有以下几个问题: 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。当然,我们也可以有其他方式解决上面的问题。 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。非阻塞的?搞一个while循环,直到insert成功再返回成功。非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。3.基于排他锁(for update)实现3.1 实现方式获取锁可以通过,在select语句后增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,我们可以认为获得排它锁的线程即可获得分布式锁;其余实现与使用唯一索引相同;释放锁通过connection.commit();操作,提交事务来实现。3.2 优点实现简单、易于理解。3.3 缺点排他锁会占用连接,产生连接爆满的问题;如果表不大,可能并不会使用行锁;同样存在单点问题、并发量问题。3.4 伪代码CREATE TABLE `methodLock` ( `id` INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` VARCHAR ( 64 ) NOT NULL DEFAULT '' COMMENT '锁定的方法名', `desc` VARCHAR ( 1024 ) NOT NULL DEFAULT '备注信息', `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY ( `id` ), UNIQUE KEY `uidx_method_name` ( `method_name ` ) USING BTREE ) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '锁定中的方法';/** * 加锁 */public boolean lock() { // 开启事务 connection.setAutoCommit(false); // 循环阻塞,等待获取锁 while (true) { // 执行获取锁的sql result = select * from methodLock where method_name = xxx for update; // 结果非空,加锁成功 if (result != null) { return true; } } // 加锁失败 return false;}/** * 解锁 */public void unlock() { // 提交事务,解锁 connection.commit();}4.乐观锁实现一般是通过为数据库表添加一个 version 字段来实现读取出数据时,将此版本号一同读出. ...

June 29, 2020 · 1 min · jiezi

分布式锁3Redisson实现

分布式锁(3)-Redisson实现文章分布式锁(2)- 基于Redis的实现中,最后给出的redis实现的分布式锁,还有一个严重的问题,那就是这种实现是不可重入的,而要实现可重入的分布式锁,会很麻烦,幸亏已经有现成的轮子可以使用。 1.Redisson简介Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。相对于Jedis而言,Redisson强大的一批。当然了,随之而来的就是它的复杂性。它里面也实现了分布式锁,而且包含多种类型的锁,更多请参阅分布式锁和同步器 2.可重入锁首先引入jar包 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.10.1</version></dependency>然后,通过配置获取`RedissonClient客户端的实例,然后getLock获取锁的实例,进行操作即可。 public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); config.useSingleServer().setPassword("redis1234"); final RedissonClient client = Redisson.create(config); RLock lock = client.getLock("lock1"); try{ lock.lock(); }finally{ lock.unlock(); }}3.获取锁实例我们先来看RLock lock = client.getLock("lock1"); 这句代码就是为了获取锁的实例,然后我们可以看到它返回的是一个RedissonLock对象。 public RLock getLock(String name) { return new RedissonLock(connectionManager.getCommandExecutor(), name);}在RedissonLock构造方法中,主要初始化一些属性。 public RedissonLock(CommandAsyncExecutor commandExecutor, String name) { super(commandExecutor, name); //命令执行器 this.commandExecutor = commandExecutor; //UUID字符串 this.id = commandExecutor.getConnectionManager().getId(); //内部锁过期时间 this.internalLockLeaseTime = commandExecutor. getConnectionManager().getCfg().getLockWatchdogTimeout(); this.entryName = id + ":" + name;}4.加锁当我们调用lock方法,定位到lockInterruptibly。在这里,完成了加锁的逻辑。 ...

June 28, 2020 · 3 min · jiezi

分布式锁2-基于Redis的实现

分布式锁(2)- 基于Redis的实现 1. 使用Redis实现分布式锁的理由Redis具有很高的性能;Redis的命令对此支持较好,实现起来很方便;2.Redis命令介绍SETNX // 当且仅当key不存在时,set一个key为val的字符串,返回1;// 若key存在,则什么都不做,返回0。SETNX key val;expire // 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。expire key timeout;delete // 删除keydelete key;我们通过Redis实现分布式锁时,主要通过上面的这三个命令。 3.分布式锁实现原理3.1 加锁最简单的方法是使用 setnx 命令。key 是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给 key 命名为 “lock_sale_商品ID” 。而 value 设置成什么呢?我们可以姑且设置成 1。加锁的伪代码如下: setnx(lock_sale_商品ID, 1)当一个线程执行 setnx 返回 1,说明 key 原本不存在,该线程成功得到了锁;当一个线程执行 setnx 返回 0,说明 key 已经存在,该线程抢锁失败。 3.2 解锁有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del 指令,伪代码如下: del(lock_sale_商品ID)释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。 3.3 锁超时锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住(死锁),别的线程再也别想进来。所以,setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx 不支持超时参数,所以需要额外的指令,伪代码如下: expire(lock_sale_商品ID, 30)综合伪代码如下: if(setnx(lock_sale_商品ID,1) == 1){ expire(lock_sale_商品ID,30) try { do something ...... } finally { del(lock_sale_商品ID) }}4.存在的问题以上伪代码中存在三个致命问题 ...

June 28, 2020 · 2 min · jiezi

通俗易懂地介绍分布式锁实现

文章来源:www.liangsonghua.me作者介绍:京东资深工程师-梁松华,长期关注稳定性保障、敏捷开发、JAVA高级、微服务架构 一般情况下我们会通过下面的方法进行资源的一致性保护 // THIS CODE IS BROKENfunction writeData(filename, data) { var lock = lockService.acquireLock(filename); if (!lock) { throw 'Failed to acquire lock'; } try { var file = storage.readFile(filename); var updated = updateContents(file, data); storage.writeFile(filename, updated); } finally { lock.release(); }}但是很遗憾的是,上面这段代码是不安全的,比如客户端client-1获取锁后由于执行垃圾回收GC导致一段时间的停顿(stop-the-word GC pause)或者其他长时间阻塞操作,此时锁过期了,其他客户如client-2会获得锁,当client-1恢复后就会出现client-1client-2同时处理获得锁的状态 我们可能会想到通过令牌或者叫版本号的方式,然而在使用Redis作为锁服务时并不能解决上述的问题。不管我们怎么修改Redlock生成token的算法,使用unique random随机数是不安全的,使用引用计数也是不安全的,一个redis node服务可能会出宕机,多个redis node服务可能会出现同步异常(go out of sync)。Redlock锁会失效的根本原因是Redis使用getimeofday作为key缓存失效时间而不是监视器(monitonic lock),服务器的时钟出现异常回退无法百分百避免,ntp分布式时间服务也是个难点 分布式锁实现需要考虑锁的排它性和不能释放它人的锁,作者不推荐使用Redlock算法,推荐使用zookeeper或者数据库事务(个人不推荐:for update性能太差了) 补充:使用zookeeper实现分布式锁 可以通过客户端尝试创建节点路径,成功就获得锁,但是性能较差。更好的方式是利用zookeeper有序临时节点,最小序列获得锁,其他节点lock时需要阻塞等待前一个节点(比自身序列小的最大那个)释放锁(countDownLatch.wait()),当触发watch事件时将计数器减一(countDownLatch.countDown()),然后此时最小序列节点将会获得锁。可以利用Curator简化操作,示例如下 public static void main(String[] args) throws Exception { //重试策略 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); //创建工厂连接 final CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(connetString) .sessionTimeoutMs(sessionTimeOut).retryPolicy(retryPolicy).build(); curatorFramework.start(); //创建分布式可重入排他锁,监听客户端为curatorFramework,锁的根节点为/locks final InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/lock"); final CountDownLatch countDownLatch = new CountDownLatch(1); for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); //加锁 mutex.acquire(); process(); } catch (Exception e) { e.printStackTrace(); }finally { try { //释放锁 mutex.release(); System.out.println(Thread.currentThread().getName() + ": release lock"); } catch (Exception e) { e.printStackTrace(); } } } },"Thread" + i).start(); } Thread.sleep(100); countDownLatch.countDown(); } }补充:redis实现分布式锁 ...

July 11, 2019 · 2 min · jiezi

幂等性的作用及实现

幂等性幂等这个词原自数学,某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。 比如,某服务记录关键数据 X,当前值为 100。A 请求需要将 X 增加 200;同时,B 请求需要将 X 减去 100。在理想的情况下,A 先读取到 X=100,然后 X 增加 200,最后 X=300。B 请求接着从读取 X=300,减去 100,最后 X=200。 然而在真实情况下,如果不做任何处理,则可能会出现:A 和 B 同时读取到 X=100;假如 A 比 B 先执行完,那么最后 X=0,如果 B 比 A 先执行完,那么最后 X=300。不管是那种情况发生了,都产生了副作用或者说是产生了不可预料的结果,并且是不可以接受的异常。这种情况就是我们提供的方法或者接口不满足幂等性,导致的不可预料的结果。 保证幂等性的方法1.建立唯一索引,防止新增脏数据 这个可以限制重复插入数据,当重复时,数据库会抛异常,保证不会出现脏数据。但体验不好,并且实用场景有限制。 2.利用 token 机制,防止页面重复提交 核心思想是为每一次操作生成一个唯一性的凭证,也就是token。一个token在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。 3.状态机幂等 在有状态的数据中可以使用,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。如果状态是顺序的,不可逆,那么就不会出现 ABA 问题,否则会出现 ABA问题。 4.select + insert 这种情况在没有并发的系统中可以解决幂等问题,在单JVM有并发的时候可以加锁来保证幂等性,在分布式环境它是没发保证幂等的,这时候需要用到分布式锁来保证。 5.分布式锁 在进入方法时,先去获取锁,假如获取到锁,就继续后面的流程。假如没有获取到锁,就等待锁的释放直到获取到锁。当执行完方法时,释放锁。当然,锁要设个超时时间,防止意外没有释放到锁。它用来解决分布式系统的幂等性,常用的实现方案是 redis 和 zookeeper 等工具。 6.对外提供幂等的接口 通过 source来源+seq序列号来判断请求是否重复, 在并发时只能处理一个请求。其它相同并发请求要么返回请求重复,要么等待前面请求执行完成在执行。 幂等性的不足1.增加了额外控制幂等的业务逻辑,复杂化了业务功能。 2.把并行执行的功能改为串行执行,降低了执行效率。 最后,幂等性虽然复杂化了业务功能和降低了执行效率,但为了保证系统的正确性,是必要的。就上面更新 X 的例子,在单台服务器上,给那段代码加上锁,并给 X 设为 volatile,就保证来数据的正确性了。在分布式环境下并且 X 是从数据库或者文件里查询出来的,用上面加锁的方式实现就不能保证数据的正确性了,这时候就需要用到分布式锁了。所以,保证方法或接口的幂等性是非常有必要的,因为数据是不能出现任何问题的。 ...

June 20, 2019 · 1 min · jiezi

曾奇谈谈我所认识的分布式锁

出品 | 滴滴技术作者 | 曾奇 前言:随着计算机技术和工程架构的发展,微服务变得越来越热。如今,绝大多数服务都处于分布式环境中,其中,数据一致性是我们一直关注的重点。分布式锁到底是什么?经过了哪些发展演进?工程上有哪些实现方案?各种方案的利弊权衡又有哪些?希望这篇文章能够对你有一些帮助。 ▍阅读索引 0.名词定义1.问题引入2.分布式环境的特点3.锁4.分布式锁5.分布式锁实现方案 5.1朴素Redis实现方案、朴素Redis方案小结5.2 ZooKeeper实现方案、ZooKeeper方案小结5.3 Redisson实现方案、Redission方案小结6.总结7.结束语8.Reference ▍0. 名词定义 分布式锁:顾名思义,是指在分布式环境下的锁,重点在锁。所以我们先从锁开始讲起。 ▍1. 问题引入 举个例子: 某服务记录数据X,当前值为100。A请求需要将X增加200;同时,B请求需要将X减100。 在理想的情况下,A先读取到X=100,然后X增加200,最后写入X=300。B请求接着读取到X=300,减少100,最后写入X=200。 然而在真实情况下,如果不做任何处理,则可能会出现:A和B同时读取到X=100;A写入之前B读取到X;B比A先写入等等情况。 上面这个例子相信大家都非常熟悉。出现不符合预期的结果本质上是对临界资源没有做好互斥操作。互斥性问题通俗来讲,就是对共享资源的抢占问题。对于共享资源争抢的正确性,锁是最常用的方式,其他的如CAS(compare and swap)等,这里不展开。 ▍2. 分布式环境的特点 我们的绝大部分服务都处于分布式环境中。那么,分布式系统有哪些特点呢?大致如下: 可扩展性:可通过横向水平扩展提高系统的性能和吞吐量。高可靠性:高容错,即使系统中一台或几台故障,系统仍可提供服务。高并发性:各机器并行独立处理和计算。廉价高效:多台小型机而非单台高性能机。▍3.锁 我们先来看下非分布式情况下的锁方案(多线程和多进程的情况),然后再演进到分布式锁。 ▍多线程下的锁机制: 各种语言有不同的实现方式,比较成熟。比如,go语言中的sync.RWMutex(读写锁)、sync.Mutex(互斥锁);JAVA中的ReentrantLock、synchronized;在php中没有找到原生的支持锁的方式,只能通过外部来间接实现,比如文件锁,借助外部存储的锁等。 ▍多进程下的锁机制: 对于临界资源的访问已经超出了单个进程的控制范围。在多进程的情况下,主要是利用操作系统层面的进程间通信原理来解决临界资源的抢占问题。比较常见的一种方法便是使用信号量(Semaphores)。 ▍对信号量的操作,主要是P操作(wait)和V操作(signal): P操作 ( wait ) :先检查信号量的大小,若值大于零,则将信号量减1,同时进程获得共享资源的访问权限,继续执行;若小于或者等于零,则该进程被阻塞后,进入等待队列。 V操作 ( signal ) :该操作将信号量的值加1,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。 可看出,多进程锁方案跟多线程的锁方案实现思路大同小异。 我们将互斥的级别拉高,分布式环境下不同节点不同进程或线程之间的互斥,就是分布式锁的挑战之一。后面再细讲。 另外,在传统的基于数据库的架构中,对于数据的抢占问题也可以通过数据库事务(ACID)来保证。在分布式环境中,出于对性能以及一致性敏感度的要求,使得分布式锁成为了一种比较常见而高效的解决方案。 ▍从上面对于多线程和多进程锁的概括,可以总结出锁的抽象条件: 1)“需要有存储锁的空间,并且锁的空间是可以访问到的”: 对于多线程就是内存(进程中不同的线程都可以读写),多进程中通过共享内存的方式,也是提供一块地方,供不同进程读写。主要目的是保证不同的进线程改动对于其他进线程可见,进而满足互斥性需求。 2)“锁需要被唯一标识”: 不同的共享资源,必然需要用不同的锁进行保护,因此相应的锁必须有唯一的标识。在多线程环境中,锁可以是一个对象,那么对这个对象的引用便是这个唯一标识。多进程下,比如有名信号量,便是用硬盘中的文件名作为唯一标识。 3)“锁要有至少两种状态”: 有锁,没锁。存在,不存在等等。很好理解。 满足上述三个条件就可以实现基础的分布式锁了。但是随着技术的演进, ▍相应地,对锁也提出了更高级的条件: 1)可重入: 外层函数获得锁之后,内层函数还可以获得锁。原因是随着软件复杂性增加,方法嵌套获取锁已经很难避免。但是从代码层面很难分析出这个问题,因此我们要使用可重入锁。导致锁需要支持可重入的场景。对于可重入的思考,每种语言有自己的哲学和取舍,如go就舍弃了支持重入:Recursive locking in Go [ https://stackoverflow.com/que... ]以后go又会不会认为“可重入真香”呢?哈哈,我们拭目以待。 2)避免产生惊群效应(Herd Effect): ...

June 3, 2019 · 2 min · jiezi

PHP-使用Redis实现锁

Last-Modified: 2019年5月10日15:31:41 参考链接PHP使用Redis+Lua脚本操作的注意事项《Redis官方文档》用Redis构建分布式锁锁实现的注意点互斥: 任意时刻, 只能有一个客户端获得锁不会死锁: 客户端持有锁期间崩溃, 没有主动解除锁, 能保证后续的其他客户端获得锁锁归属标识: 加锁和解锁的必须是同一个客户端, 客户端不能解掉非自己持有的锁(锁应具备标识)如果是Redis集群, 还得考虑具有容错性: 只要大部分Redis节点正常运行, 客户端就可以加锁和解锁. 以下只考虑 Redis单机部署的 场景. 如果是Redis集群部署, 可以使用 加锁php 加锁示例 $redis = new Redis();$redis->pconnect("127.0.0.1", 6379);$redis->auth("password"); // 密码验证$redis->select(1); // 选择所使用的数据库, 默认有16个$key = "...";$value = "...";$expire = 3;// 参数解释 ↓// $value 加锁的客户端请求标识, 必须保证在所有获取锁清秋的客户端里保持唯一, 满足上面的第3个条件: 加锁/解锁的是同一客户端// "NX" 仅在key不存在时加锁, 满足条件1: 互斥型// "EX" 设置锁过期时间, 满足条件2: 避免死锁$redis->set($key, $value, ["NX", "EX" => $expire])执行上面代码结果: $key 对应的锁不存在, 进行加锁操作$key 对应的锁已存在, 什么也不做加锁容易错误的点: 使用 setnx 和 expire 的组合原因: 若在 setnx 后脚本崩溃会导致死锁 ...

May 10, 2019 · 2 min · jiezi

百度社招面试题——如何用Redis实现分布式锁

关于Redis实现分布式锁的问题,网络上很多,但是很多人的讨论基本就是把原来博主的贴过来,甚至很多面试官也是一知半解经不起推敲就来面候选人,最近结合我自己的学习和资料查阅,整理一下用Redis实现分布式锁的方法,欢迎评论、交流、讨论。1.单机Redis实现分布式锁1.1获取锁获取锁的过程很简单,客户端向Redis发送命令:SET resource_name my_random_value NX PX 30000my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。PX 30000表示这个锁有一个30秒的自动过期时间。1.2 释放锁if redis.call(“get”,KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1])else return 0end之前获取锁的时候生成的my_random_value 作为参数传到Lua脚本里面,作为:ARGV[1],而 resource_name作为KEYS[1]。Lua脚本可以保证操作的原子性。1.3 关于单点Redis实现分布式锁的讨论网络上有文章说用如下命令获取锁:SETNX resource_name my_random_valueEXPIRE resource_name 30由于这两个命令不是原子的。如果客户端在执行完SETNX后crash了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁,其他的客户端就永远获取不到这个锁了。为什么my_random_value 要设置成随机值?保证了一个客户端释放的锁是自己持有的那个锁。如若不然,可能出现锁不安全的情况。客户端1获取锁成功。客户端1在某个操作上阻塞了很长时间。过期时间到了,锁自动释放了。客户端2获取到了对应同一个资源的锁。客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。用 SETNX获取锁网上大量文章说用如下命令获取锁:SETNX lock.foo <current Unix time + lock timeout + 1>原文在Redis对SETNX的官网说明,Redis官网文档建议用Set命令来代替,主要原因是SETNX不支持超时时间的设置。https://redis.io/commands/setnx2.Redis集群实现分布式锁上面的讨论中我们有一个非常重要的假设:Redis是单点的。如果Redis是集群模式,我们考虑如下场景:客户端1从Master获取了锁。Master宕机了,存储锁的key还没有来得及同步到Slave上。Slave升级为Master。客户端2从新的Master获取到了对应同一个资源的锁。客户端1和客户端2同时持有了同一个资源的锁,锁不再具有安全性。就此问题,Redis作者antirez写了RedLock算法来解决这种问题。2.1 RedLock获取锁获取当前时间。按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的单机Redis Lua脚本释放锁的方法)。2.2 RedLock释放锁客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。2.3 关于RedLock的问题讨论如果有节点发生崩溃重启假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。节点C重启后,客户端2锁住了C, D, E,获取锁成功。客户端1和客户端2同时获得了锁。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。如果客户端长期阻塞导致锁过期解释一下这个时序图,客户端1在获得锁之后发生了很长时间的GC pause,在此期间,它获得的锁过期了,而客户端2获得了锁。当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源(上图中是一个存储服务)发起了写数据请求,而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。如何解决这个问题呢?引入了fencing token的概念:客户端1先获取到的锁,因此有一个较小的fencing token,等于33,而客户端2后获取到的锁,有一个较大的fencing token,等于34。客户端1从GC pause中恢复过来之后,依然是向存储服务发送访问请求,但是带了fencing token = 33。存储服务发现它之前已经处理过34的请求,所以会拒绝掉这次33的请求。这样就避免了冲突。但是其实这已经超出了Redis实现分布式锁的范围,单纯用Redis没有命令来实现生成Token。时钟跳跃问题假设有5个Redis节点A, B, C, D, E。客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。客户端1和客户端2现在都认为自己持有了锁。这个问题用Redis实现分布式锁暂时无解。而生产环境这种情况是存在的。结论Redis并不能实现严格意义上的分布式锁。但是这并不意味着上面讨论的方案一无是处。如果你的应用场景为了效率(efficiency),协调各个客户端避免做重复的工作,即使锁失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。但是如果你的应用场景是为了正确性(correctness),那么用Redis实现分布式锁并不合适,会存在各种各样的问题,且解决起来就很复杂,为了正确性,需要使用zab、raft共识算法,或者使用带有事务的数据库来实现严格意义上的分布式锁。参考资料Distributed locks with Redis基于Redis的分布式锁到底安全吗(上)? - 铁蕾的个人博客https://martin.kleppmann.com/…热门阅读技术文章汇总【Leetcode】101. 对称二叉树【Leetcode】100. 相同的树【Leetcode】98. 验证二叉搜索树 ...

April 10, 2019 · 1 min · jiezi

女朋友也能看懂的Zookeeper分布式锁原理

前言关于分布式锁,在互联网行业的使用场景还是比较多的,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景。而实现分布式锁的手段也很多,大家比较常见的就是redis跟zookeeper,今天我们主要介绍的是基于zookeeper实现的分布式锁。这篇文章主要借用Curator框架对zk分布式锁的实现思路,大家理解了以后完全可以自己手动实现一遍,但是在工作中还是建议使用成熟的开源框架,很多坑别人已经帮我们踩好了,除非万不得已,需要高度定制符合自己项目的需求的时候,才开始自行封装吧。正文zookeeper简单介绍既然是基于zookeeper的分布式锁,首先肯定要对这个zookeeper有一定了解,这里就不过多的进行讲解,只对其跟分布式锁有关联的特性做一个简单的介绍,更多详细的功能特性大家可以参阅官方文档。zookeeper维护着类似文件系统的数据结构,它总共有四种类型的节点PERSISTENT:持久化的节点。一旦创建后,即使客户端与zk断开了连接,该节点依然存在。PERSISTENT_SEQUENTIAL:持久化顺序编号节点。比PERSISTENT节点多了节点自动按照顺序编号。EPHEMERAL:临时节点。当客户端与zk断开连接之后,该节点就被删除。EPHEMERAL_SEQUENTIAL:临时顺序编号节点。比EPHEMERAL节点多了节点自动按照顺序编号。(分布式锁实现使用该节点类型)Curator实现分布式锁原理好,当我们简单了解了zk的节点类型以后,现在正式的分析Curator分布式锁的实现原理。这里我们定义了一个“/curator_lock”锁节点用来存放相关客户端创建的临时顺序节点。 假设两个客户端ClientA跟ClientB同时去争夺一个锁,此时ClientA先行一步获得了锁,那么它将会在我们的zk上创建一个“/curator_lock/xxxxx-0000000000”的临时顺序节点。接着它会拿到“/curator_lock/”锁节点下的所有子节点,因为这些节点是有序的,这时候会判断它所创建的节点是否排在第一位(也就是序号最小),由于ClientA是第一个创建节点的的客户端,必然是排在第一位,所以它也就拿到了锁。[zk: localhost:2182(CONNECTED) 4] ls /curator_lock[_c_f3f38067-8bff-47ef-9628-e638cfaad77e-lock-0000000000]这个时候ClientB也来了,按照同样的步骤,先是在“/curator_lock/”下创建一个临时顺序节点“/curator_lock/xxxxx-0000000001”,接着也是获得该节点下的所有子节点信息,并比对自己生成的节点序号是否最小,由于此时序号最小的节点是ClientA创建的,并且还没释放掉,所以ClientB自己就拿不到锁。[zk: localhost:2182(CONNECTED) 4] ls /curator_lock[_c_2a8198e4-2039-4a3c-8606-39c65790d637-lock-0000000001,_c_f3f38067-8bff-47ef-9628-e638cfaad77e-lock-0000000000]既然ClientB拿不到锁,也不会放弃,它会对自己的前一个节点加上监听器(zk提供的api实现),只要监听到前一个节点被删除了,也就是释放了锁,就会马上重新执行获取锁的操作。当后面的ClientC,ClientD…过来的时候也是如此,变化的只是节点上的编号,它会根据Client连接的数量而不断增加。可能大家还会担心,万一我的获取到锁的客户端宕机了怎么办,会不会不释放锁?其实上面已经解答了这个问题,由于Curator使用的是临时顺序节点来实现的分布式锁,只要客户端与zk连接断开,该节点也就消失了,相当于释放了锁。下面代码展示了Curator的基本使用方法,仅作为参考实例,请勿在生产环境使用的这么随意。CuratorFramework client = CuratorFrameworkFactory.newClient(“127.0.0.1:2182”, 5000,10000, new ExponentialBackoffRetry(1000, 3)); client.start(); InterProcessMutex interProcessMutex = new InterProcessMutex(client, “/curator_lock”); //加锁 interProcessMutex.acquire(); //业务逻辑 //释放锁 interProcessMutex.release(); client.close(); 总结我们在搞懂了原理之后,就可以抛弃Curator,自己动手实现一个分布式锁了,相信大家实现基本的功能都是没问题的,但是要做到生产级别,可能还是要在细节上下功夫,比如说一些异常处理,性能优化等因素。微信公众号《深夜里的程序猿》 - 分享最干的干货

April 10, 2019 · 1 min · jiezi

用分布式锁解决并发问题

在系统中,当存在多个进程和线程可以改变某个共享数据时,就容易出现并发问题导致共享数据的不一致性。即多个进程同时获取到了对数据的操作权限并对数据进行了更新,很典型的场景就是在线销售系统在售卖热销商品时遇到多个并发请求在同一时间提交订单的情况则极有可能造成商品超卖的现象。只要访问流量不错的系统都有可能遭遇并发请求造成数据库中数据重复写入的情况。针对程序块被多个进程并发执行问题的解决方案是确保同一个时刻同一个程序块只能有一个进程可执行,其他进程等待当前进程执行完成才能获取程序块的执行权对数据进行更新,以此类推将并发执行变为串行顺序执行。为了让获取执行权的进程不被其他干扰,就需要设置一个所有进程都能读取到的标记,当标记不存在时可以设置该标记,其余后续进程发现已经有标记了则等待拥有标记的进程结束执行程序块取消标记后再去尝试设置标记。这个标记可以理解为锁,设置标记的过程就是我们通常说的加锁。用redis 的 setnx、expire 方法做分布式锁setnx()setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。expire()expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。具体步骤1、setnx(lockKey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功2、expire() 命令对 lockKey 设置超时时间,为的是避免死锁问题。3、执行完业务代码后,可以通过 delete 命令删除 key。这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。用 redis 的 setnx()、get()、getset()方法做分布式锁这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化。getset()这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2依次类推!使用步骤setnx(lockKey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转到步骤 2。get(lockKey) 获取值,值是当前lockKey的过期时间用oldExpireTime代表 ,并将这个 oldExpireTime与当前的系统时间进行比较,如果早于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 步骤3,否则等待指定时间后返回步骤2重新开始判定。计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockKey, newExpireTime) 会返回当前 lockKey 之前设置的旧值currentExpireTime。判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前进程getset 设置锁成功,获取到了锁。如果不相等,说明这个锁已经被别的进程获取走了,那么当前请求可以根据具体需求逻辑直接返回失败,或者返回步骤2继续重试。在获取到锁之后,当前进程可以开始自己的业务处理,当处理完毕后,比较当前理时间和对锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,锁可能已由其他进程获得,这时执行 delete释放锁的操作会导致把其他进程已获得的锁释放掉。下面是用PHP代码实现的Redis分布式锁,关于Redis部分使用的是伪代码,请根据自己的情况用Redis连接对象替代其中的伪代码。/** * 获取Redis分布式锁 * * @param $lockKey * @return bool /function getRedisDistributedLock(string $lockKey) : bool{ $lockTimeout = 2000;// 锁的超时时间2000毫秒 $now = intval(microtime(true) * 1000); $lockExpireTime = $now + $lockTimeout; $lockResult = Redis::setnx($lockKey, $lockExpireTime); if ($lockResult) { // 当前进程设置锁成功 return true; } else { $oldLockExpireTime = Redis::get($lockKey); if ($now > $oldLockExpireTime && $oldLockExpireTime == Redis::getset($lockKey, $lockExpireTime)) { return true; } } return false;}/* * 串行执行程序 * * @param string $lockKey Key for lock * @param Closure $closure 获得锁后进程要执行的闭包 * @return mixed */function serialProcessing(string $lockKey, Closure $closure){ if (getRedisDistributedLock($lockKey)) { $result = $closure(); $now = intval(microtime(true) * 1000); if ($now < Redis::get($lockKey)) { Redis::del($lockKey); } } else { // 延迟200毫秒再执行 usleep(200 * 1000); return serialProcessing($lockKey, $closure); } return $result;}上面serialProcessing方法里当前进程设置锁成功,获取了代码块的执行权后就会执行闭包参数$closure里的代码块,通过传递闭包给方法,让我们可以在项目任何需要确保程序串行执行的地方使用serialProcessing方法给程序加分布式锁解决并发请求的问题。上面代码实现用面向过程的方式是为了能简单明了的描述怎么设置分布式锁,读者可以针对自己的情况执行设计实现代码。针对于大型系统使用集群Redis的情况,设置分布式锁的步骤更复杂,有兴趣的可以查看Redlock 算法和redissonredis分布式锁组件。 ...

March 13, 2019 · 2 min · jiezi

Redis集群环境下的-RedLock(真分布式锁) 实践

在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。 有很多三方库和文章描述如何用Redis实现一个分布式锁管理器,但是这些库实现的方式差别很大,而且很多简单的实现其实只需采用稍微增加一点复杂的设计就可以获得更好的可靠性。 这篇文章的目的就是尝试提出一种官方权威的用Redis实现分布式锁管理器的算法,我们把这个算法称为RedLock。Redlock是redis官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。关于这个算法的讨论可以看下官方文档。https://github.com/antirez/re…安全和可靠性保证在描述我们的设计之前,我们想先提出三个属性,这三个属性在我们看来,是实现高效分布式锁的基础。1、一致性:互斥,不管任何时候,只有一个客户端能持有同一个锁。2、分区可容忍性:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。3、可用性:只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。为什么基于故障切换的方案不够好为了理解我们想要提高的到底是什么,我们先看下当前大多数基于Redis的分布式锁三方库的现状。 用Redis来实现分布式锁最简单的方式就是在实例里创建一个键值,创建出来的键值一般都是有一个超时时间的(这个是Redis自带的超时特性),所以每个锁最终都会释放。而当一个客户端想要释放锁时,它只需要删除这个键值即可。 表面来看,这个方法似乎很管用,但是这里存在一个问题:在我们的系统架构里存在一个单点故障,如果Redis的master节点宕机了怎么办呢?有人可能会说:加一个slave节点!在master宕机时用slave就行了!但是其实这个方案明显是不可行的,因为这种方案无法保证第1个安全互斥属性,因为Redis的复制是异步的。 总的来说,这个方案里有一个明显的竞争条件(race condition),举例来说:1、客户端A在master节点拿到了锁。2、master节点在把A创建的key写入slave之前宕机了。3、slave变成了master节点4、B也得到了和A还持有的相同的锁(因为原来的slave里还没有A持有锁的信息)当然,在某些特殊场景下,前面提到的这个方案则完全没有问题,比如在宕机期间,多个客户端允许同时都持有锁,如果你可以容忍这个问题的话,那用这个基于复制的方案就完全没有问题,否则的话我们还是建议你采用这篇文章里接下来要描述的方案。Redlock 简介在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。实现高效的分布式锁有三个属性需要考虑:1、安全属性:互斥,不管什么时候,只有一个客户端持有锁2、效率属性A:不会死锁3、效率属性B:容错,只要大多数redis节点能够正常工作,客户端端都能获取和释放锁。Redlock 算法在分布式版本的算法里我们假设我们有N个Redis master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把N设成5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:1、获取当前时间(单位是毫秒)。2、轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。Redisson 实现方式(红锁 RedLock)github Redisson https://github.com/redisson/r...Maven<!-- JDK 1.8+ compatible –><dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.9.0</version></dependency> <!– JDK 1.6+ compatible –><dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>2.14.0</version></dependency>集群模式配置集群模式除了适用于Redis集群环境,也适用于任何云计算服务商提供的集群模式,例如AWS ElastiCache集群版、Azure Redis Cache和阿里云(Aliyun)的云数据库Redis版。程序化配置集群的用法:@Beanpublic RedissonClient redissonClient() { Config config = new Config(); config.useClusterServers() .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒 //可以用"rediss://“来启用SSL连接 .addNodeAddress(“redis://127.0.0.1:7000”, “redis://127.0.0.1:7001”) .addNodeAddress(“redis://127.0.0.1:7002”); return Redisson.create(config);}基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。RLock lock1 = redissonClient1.getLock(“lock1”);RLock lock2 = redissonClient2.getLock(“lock2”);RLock lock3 = redissonClient3.getLock(“lock3”);RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);// 同时加锁:lock1 lock2 lock3// 红锁在大部分节点上加锁成功就算成功。lock.lock();…lock.unlock();Redisson 监控锁大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开lock.lock(10, TimeUnit.SECONDS);// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);…lock.unlock();往期精彩文章Redis服务器被攻击后该如何安全加固MySQL从删库到恢复,还用跑路吗?理解JWT鉴权的应用场景及使用建议浅谈偏向锁、轻量级锁、重量级锁算法:一致性哈希算法的理解与实践架构:通过案例读懂 RESTful 架构风格架构:一文读懂Apache Flink架构及特性分析架构:大数据推荐系统实时架构和离线架构微服务:架构下静态数据通用缓存机制微服务:小型系统如何“微服务”开发微服务:深入理解为什么要设计幂等性的服务中间件:应用消息中间件设计可以解决哪些实际问题? ...

November 12, 2018 · 1 min · jiezi