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

起源:《全新的分布式锁,性能简略且弱小》前言:分布式锁是分布式系统中一个极为重要的工具。目前有多种分布式锁的设计方案,比方借助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