目录

1、多线程并发问题

2、普通锁

3、分布式锁

4、分布式锁解决方案

4.1、Redis分布式锁(常用)

版本一:setnx key value

版本二:set key value nx ex seconds(解决死锁问题)

版本三:分布式锁需要满足谁申请谁释放,不能释放别人的锁。(解决A释放B的锁的问题)

版本四:使用Lua脚本释放锁

版本五:Redission实现集群下的分布式锁

Jedis实现分布式锁

RedisTemplate实现分布式锁

4.2、数据库分布式锁

基于表记录实现分布式锁

基于乐观锁实现分布式锁

基于悲观锁实现分布式锁

4.3、Zookeeper分布式锁

ZNode节点

ZNode节点类型

watch监听机制

分布式锁-版本一

分布式锁-版本二


1、多线程并发问题

多线程并发情况下,如果存在对共享资源的访问,就需要给共享资源加上锁,保证共享资源同一时间只能被一个线程访问。

举例说明:

以车站卖票举例,车站有5个窗口同时卖票,现在只剩3张票(票总数为count)。

窗口卖票时,都会执行两个动作:1、判断是否有票(判断count值是否为0);2、如果还有票,则卖一张票给用户,并且票总数count-1;如果没有票,告知用户没有票。

此时有5个人(5个线程)同时去5个窗口买票,每个窗口首先判断是否有票,每个窗口同时去读取车票库存,都读取到车票剩余count=3,则5个窗口都认为还有票,于是都会卖一张票给用户,就会导致5个人都能买到票,但实际车票只有3张,造成车票超卖情况。

2、普通锁

在Java单机应用程序中,最常见的就是通过synchronized直接给共享资源的访问加锁,加锁后,同一时间只允许一个线程进行访问。

拿车票例子来说,即可以把“判断是否有票”和“卖票”两个动作加锁,即同一时间,只能有一个线程执行这两个操作。比如窗口A获得锁 -> 窗口A请求获取车票库存数 -> 窗口BCDE请求获取车票库存数(被拒绝,因为没有获得锁,阻塞,等待锁的释放) -> 窗口A卖票,库存减一 -> 窗口A释放锁 ->窗口BCDE抢夺锁,抢到锁的窗口进行卖票操作。

普通锁只针对于单机应用情况,而在分布式环境下,每个服务的运行都是独立的,不同服务有可能还会部署在不同的服务器下。此时,如果还是用synchronized锁或ReentrantLock普通锁,就无法对共享资源进行加锁操作,因为这些锁都只能锁住当前JVM环境下的进程(不同JVM里的普通锁是独立的)。此时,就需要使用分布式锁。

3、分布式锁

分布式环境下,我们可以将获取锁和释放锁的操作都交给第三方组件来处理。每一台服务器都将获取锁和释放锁的请求交给第三方组件。获取锁后,才能对分布式环境下的共享资源进行相关操作,操作完后,再对第三方组件发起释放锁的请求。这样就能实现分布式环境下共享资源的安全访问。

分布式锁需要满足以下条件:

  • 互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
  • 安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
  • 死锁:获取锁的客户端因为某些原因(如down机等)而未能释放锁,其它客户端再也无法获取到该锁。
  • 容错:当部分节点(redis节点等)down机时,客户端仍然能够获取锁和释放锁。

4、分布式锁解决方案

4.1、Redis分布式锁(常用)

版本一:setnx key value

获取锁:将业务ID作为key,如果key不存在就插入成功,并设置key存活时间并返回1,如果key已经存在就返回0。客户端通过返回值判断是否获得锁,返回值为1表示获得锁。

业务处理。

释放锁:del key。

问题:如果获取锁的服务在获取锁后挂掉,无法主动释放锁,那么锁就会一直被占用,永远也不会被释放,导致死锁。

版本二:set key value nx ex seconds(解决死锁问题)

set key的同时设置key的过期时间,如果服务器无法主动释放做,那么在一定时间后,redis主动将key过期删除,以解决死锁问题。

  • SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
  • EX seconds − 设置指定的到期时间(以秒为单位)
  • PX milliseconds − 设置指定的到期时间(以毫秒为单位)
  • NX − 仅在键不存在时设置键
  • XX − 只有在键已存在时才设置

问题:服务器A释放服务器B的锁问题。举例说明:

  • 设置锁的过期时间为10s,服务的业务处理时间为15s;
  • 服务器A获取到锁,处理业务逻辑;
  • 10s时,锁过期,被Redis自动删除,但服务器A还在处理业务;
  • 10s时,服务器B获取锁,处理业务逻辑;
  • 15s时,服务器A业务处理完毕,执行释放锁的操作,但此时锁在服务器B手里,所以服务器A释放的是服务器B的锁。

版本三:分布式锁需要满足谁申请谁释放,不能释放别人的锁。(解决A释放B的锁的问题)

在释放(删除)锁时,分两步操作执行:a、先检查锁是不是属于自己的(比如把服务器信息存到value中);b、如果是自己的,则释放锁;如果不是自己的,则不做任何操作。

问题:检查锁和释放锁是两个操作,不是原子性的。高并发情况下,可能存在检查锁的时候,锁是自己的,释放锁的时候,锁不是自己的这种情况。

版本四:使用Lua脚本释放锁

将版本三中,检查锁和释放锁的操作写在Lua脚本中,Redis执行的Lua脚本,一定是原子性的。

问题:Redis集群环境下,如果主节点还没来得及把刚刚set的数据复制给从节点就挂了,从节点升级为主节点后,就会丢失原主节点set的锁数据,就会出现多个客户端同时持有同一个资源锁的情况。

版本五:Redission实现集群下的分布式锁

红锁(RedLock):采用主节点过半机制,即获取锁或者释放锁成功的标志为:在过半的节点上操作成功,以解决版本四中的问题。

核心API:lock()和unlock()、看门狗机制。

  • 红锁,用于Redis集群,加锁或解锁时,只有集群中过半的Redis操作成功,加解锁才算成功。
  • Redisson所有指令都通过lua脚本执行,保证了操作的原子性。
  • 看门狗机制,默认检查锁的超时时间是30s,如果服务实例加锁成功后宕机,则30s后自动释放锁。
  • 看门狗机制,有锁自动续期功能,如果业务执行时间太长,每隔10s就会自动给锁续上新的30s,避免锁到期,但业务还没执行完的情况发生。
// 1、获取一把红锁锁
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
RLock lock4 = redisson.getLock("lock4");
RLock lock5 = redisson.getLock("lock5");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);// 2、加锁
// 2.1、 加锁,如果没有手动释放锁,则30s后自动删除。此时,看门狗机制会失效,不会自动续期。
lock.lock(30,TimeUnit.SECONDS);// 2.2、为加锁等待100秒时间,并在加锁成功10秒钟后自动解开。
lock.tryLock(100, 10, TimeUnit.SECONDS);// 2.3、 默认加的锁都是30s时间。看门狗机制自动续期。每隔10s续期为30s。
// 加锁业务完成后,不再续期,等待手动解锁或30s后自动删除
lock.lock();

Jedis实现分布式锁

private static final Long RELEASE_SUCCESS = 1L;/*** 尝试获取分布式锁** @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);return LOCK_SUCCESS.equals(result);
}/*** 释放分布式锁** @param jedis     redis客户端* @param lockKey   锁* @param requestId 请求标识* @return*/
// Lua脚本如下
// if redis.call("get",KEYS[1]) == ARGV[1] then
//     return redis.call("del",KEYS[1])
// else
//     return 0
// end
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {// 使用Lua脚本保证原子性,根据requestId先判断lockKey是否由自己设置,然后再决定是否删除(只能删除自己的锁)String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));return RELEASE_SUCCESS.equals(result);
}

RedisTemplate实现分布式锁

@Autowired
private RedisTemplate redisTemplate;private static final Long RELEASE_SUCCESS = 1L;/*** 尝试获取分布式锁** @param lockKey    锁* @param requestId  请求标识* @param expireTime 超时时间* @param unit         时间单位* @return*/
public static boolean tryGetDistributedLock(String lockKey, String requestId, long expireTime, TimeUnit unit) {return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expreTime, unit);
}/*** 释放分布式锁** @param lockKey   锁* @param requestId 请求标识* @return*/
public static boolean releaseDistributedLock(String lockKey, String requestId) {// 使用Lua脚本保证原子性,根据requestId先判断lockKey是否由自己设置,然后再决定是否删除(只能删除自己的锁)String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);Object execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);return RELEASE_SUCCESS.equals(execute);
}

4.2、数据库分布式锁

基于表记录实现分布式锁

创建一张表,给表中某个字段(lockName)增加唯一约束。当我们想要获得锁的时候,就可以在该表中增加一条记录,如果insert成功表示获得锁;释放锁的时候就删除这条记录。

步骤1--加锁:INSERT INTO database_lock(lockName) VALUES ('lockName');
步骤2--解锁:DELETE FROM database_lock WHERE lockName='lockName';

        问题:

        记录不会自动删除,意味着锁没有失效时间,只能手动删除;(可以用定时任务清理过期锁)

        数据库需要集群部署、支持数据同步、主备切换;

        不具备可重入特性,获取锁后,行数据一直存在;

        不具备阻塞特性,获取不到锁会直接返回失败。(可以在业务逻辑里用while和sleep解决)

基于乐观锁实现分布式锁

乐观锁:只在数据更新操作提交的时候进行冲突检测。通常通过增加version版本号列实现乐观锁。在更新前,会先读取数据,记录数据中的版本号(version列),更新时,将读取到的版本号与数据库的版本号做比较,如果一致就更新成功,否则就说明数据已被其他线程更新过,更新失败。

拿库存扣减举例,创建一个商品表有id(商品id)、resource(库存)、version(版本号)三个字段,用户购买时会对resource进行减一操作,同时version加一。那么操作可以简化为如下步骤:

// 乐观锁的实现需要确保表中有相应的数据:
INSERT INTO resource_lock(id, resource, version) VALUES(1, 10, 1);// 步骤1--获取资源:
SELECT id, resource, version as oldVersion FROM resource_lock WHERE id = 1// 步骤2--执行业务逻辑// 步骤3--更新资源:重点在于更新数据时校验数据行版本号是否与之前读取版本号一致
UPDATE resource_lock SET resource = resource - 1 and version = version + 1 WHERE id = 1 AND version = oldVersion

问题:

        需要在表中增加额外的字段,增加数据库的冗余;

        高并发时,version值在频繁变化,会导致大量数据库更新请求失败,影响系统可用性。

基于悲观锁实现分布式锁

悲观锁,认为每次更新都会发生冲突。通过在查询语句后面增加FOR UPDATE,数据库会在查询过程中给数据库表增加悲观锁,也称排他锁。当某条记录被加上悲观锁之后,其它线程也就无法再改行上增加悲观锁。

使用悲观锁时,需要关闭数据库的自动提交属性:SET AUTOCOMMIT = 0。

具体操作步骤如下所示:

// 步骤1-获取锁
SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;。// 步骤2-执行业务逻辑// 步骤3:手动提交事务,释放锁
COMMIT

两个线程A和B:A先于B执行,如果B在A释放锁之前执行步骤1,那么B会被阻塞(长时间阻塞会抛异常),直到A释放锁。

问题:

        请求获取锁成功:数据库增加加锁的额外开销;

        请求获取锁失败:线程阻塞,等待锁的释放,高并发情况下,可能有大量请求阻塞;

        查询时理想情况下是通过索引列加行锁,但是也存在数据库加表锁的情况(Mysql执行引擎认为全表扫描效率更高)。

4.3、Zookeeper分布式锁

ZNode节点

Zookeeper以ZNode节点为基本存储单元,可分为父节点和子节点,按层级存储数据。类似于windows的文件系统,不同的是,Zookeeper下所有节点均为ZNode节点,都可以理解为文件夹,只不过这个文件夹可以存储数据。

ZNode节点类型

  • 临时节点:客户端与zookeeper断开连接后,该节点会自动删除
  • 临时顺序节点:客户端与zookeeper断开连接后,该节点会自动删除,但是这些节点都是有序排列的。
  • 持久节点:客户端与zookeeper断开连接后,该节点依然存在
  • 持久顺序节点:客户端与zookeeper断开连接后,该节点依然存在,但是这些节点都是有序排列的。

watch监听机制

客户端连接Zookeeper时,可以注册监听它关心的ZNode节点,当监听的节点发生变化时(更新、删除、增加子节点),Zookeeper会通知客户端。

分布式锁-版本一

通过创建临时节点实现分布式锁。

多个客户端同时去创建同一个临时节点,谁创建成功,谁就能获取锁;获取失败的客户端就监听这个临时节点的变化,等待该临时节点被删除。

举例说明:

  1. 现有ABCD四个客户端,建立与Zookeeper的连接;
  2. ABCD同时去创建ZNode临时节点node1,A创建成功,获取锁;BCD创建失败,监听node1节点;
  3. A断开与Zookeeper的连接,Zookeeper自动删除临时节点node1,BCD被唤醒,同时去创建临时节点node1;
  4. 重复步骤1-3。

问题:惊群效应

        假设有5000个客户端同时创建临时节点,当有一个客户端创建成功后,其余4999个客户端都会监听该临时节点;当该临时节点被删除后,会唤醒4999个客户端,来重新竞争节点的创建,但只有1个能创建成功。(一个节点被释放,却要惊动其余所有客户端?造成Zookeeper压力过大,并且浪费客户端的线程资源。)

分布式锁-版本二

通过创建临时顺序节点实现分布式锁,解决惊群效应问题。

多个客户端同时去一个ZNode节点下创建节点,都能创建成功,只是创建的节点带有顺序编号,比如0001,0002,0003,相当于限定各客户端获取锁的顺序。编号小的先获取锁,未获取到锁的只需要监听他的前一个节点的删除变化。

举例说明:

  1. 现有ABCD四个客户端建立了与Zookeeper的连接;
  2. ABCD同时去root节点下分别创建临时顺序节点0001,0002,0003,0004;
  3. A的序号最小,获取锁;BCD获取失败,根据序号大小,B监听A,C监听B,D监听C;
  4. A断开与Zookeeper的连接,删除0001节点,B被唤醒,获取锁;
  5. B断开与Zookeeper的连接,删除0002节点,C被唤醒,获取锁;
  6. C断开与Zookeeper的连接,删除0003节点,D被唤醒,获取锁。

以上内容为个人学习总结,仅供学习参考,如有问题,欢迎在评论区指出,谢谢!

Java分布式锁解决方案相关推荐

  1. java分布式锁解决方案 redisson or ZooKeeper

    redis 分布式锁 Redisson 是 redis 官方推荐的Java分布式锁第三方框架. 高效分布式锁 当我们在设计分布式锁的时候,我们应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效的设 ...

  2. 面试官:Redis分布式锁解决方案是什么?

    今天博主在这片文章中主要给大家讲下Redis分布式锁的原理以及解决方案 学到三连呦 1.Redis分布式锁原理 1.1.简述 我们知道分布式锁的特性是排他.避免死锁.高可用.分布式锁的实现可以通过数据 ...

  3. 高并发系统设计——分布式锁解决方案

    摘要 分布式应用进行逻辑处理时经常会遇到并发问题.比如一个操作要修改用户的状态,修改状态需要先读出用户的状态, 在内存里进行修改,改完了再存回去.如果这样的操作同时进行了,就会出现并发问题, 因为读取 ...

  4. DB数据变更缓存分布式更新的zk分布式锁解决方案

    DB数据变更缓存分布式更新的zk分布式锁解决方案 KafkaConsumer kafak消费线程,DB数据变更后,将消息推送到kafka topic,由消费线程进行消费 属性  1.ConsumerC ...

  5. java分布式锁终极解决方案之 redisson

    目前有很多项目还在使用redis的 setNx 充当分布式锁,然而这个锁是有问题的,redisson是java支持redis的redlock的唯一实现,.目前支持集群模式,云托管模式,单Redis节点 ...

  6. Java分布式锁那点事

    分布式锁那点事 为什么要使用分布式锁 为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或 ...

  7. 搞懂Java分布式锁实现看这篇文章就对了

    2019独角兽企业重金招聘Python工程师标准>>> 前言: 随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理 ...

  8. Java分布式锁看这篇就够了,java基础面试笔试题

    我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家. 扫描二维码或搜索下图红色VX号,加VX好友,拉你进[程序员面试学习交流群]免费领取.也欢迎各位一起 ...

  9. Java分布式锁的概念以及使用优点

    普通的锁,即在单机多线程环境下,当多个线程需要访问同一个变量或代码片段时,被访问的变量或代码片段叫做临界区域,我们需要控制线程一个一个的顺序执行,否则会出现并发问题. 设置一个各个线程都能看的见的标志 ...

最新文章

  1. ECCV2020|超快的车道线检测,代码模型已开源
  2. XenDesktop7-基于SCVMM2012SP1的部署
  3. oracle 连接池sql跟踪,实现SQLServer、MySQL和oracle数据库连接池
  4. 射影几何笔记6:齐次坐标下“点-线”几何关系
  5. python输出去空格_Python3基础 print(,end=) 输出内容的末尾加入空格
  6. mybatis方法传入多参数
  7. IOS应用管理学习,进阶,涉及字典转模型,工厂方法,面向对象思想,页面布局等
  8. ACM-ICPC 2018 徐州赛区网络预赛 D. EasyMath
  9. 反射 getDeclaredMethod和getMethod的区别以及用法《实例》
  10. 《人机交互与戏剧表演:用戏剧理论构建良好用户体验》一导读
  11. matlab彩色图像变暗
  12. 微信小程序 获取php值,微信小程序如何获取javascript里的数据
  13. Kubernetes 小白学习笔记(20)--kubernetes的运维-管理Node
  14. 论文篇-----基于机器学习的交通流预测技术的研究与应用
  15. DCMI接口之OV2640摄像头
  16. 人民币转换美金的c语言代码大全,美元换算(人民币换算)
  17. ajax的三种传参方式
  18. java+selnium爬取凡人修仙传
  19. 微信有哪些隐藏功能?实用隐藏功能合集:建小号、批量群发
  20. .net的过去、现在和未来

热门文章

  1. html网页设计作业代码——代理商销售管理系统后台(8页) HTML+CSS+JavaScript web前端设计与开发期末作品_期末大作业
  2. Groovy——def
  3. pycharm、IDEA、PhpStorm、WebStorm(JetBrains全家桶)如何上传代码到github
  4. lasso回归操作步骤
  5. VirtualAllocEx;WriteProcessMemory;CreateRemoteThread
  6. linux 卸载 字体,在Ubuntu中如何更换字体
  7. mysql hibernate mediumtext_mysql中的text,mediumtext,longtext在Hibernate中的類型映射 | 學步園...
  8. 记录从U盘安装Ubuntu20.04系统到旧电脑
  9. php打乱数组顺序(含二维数组)
  10. hibernate @Where注解