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

**Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...

Service中@Transactional注解和synchronized关键字的问题

问题示例

就先举(编)个例子:

@Transactional(rollbackFor = Exception

这段代码存在的问题:原本可能他是想要在执行exampleDemo()方法的时候,保证这个线程是安全的。

为什么我会说这段代码存在问题?

因为@Transactional(rollbackFor = Exception.class)是事务操作,事务的范围比synchronized范围大,当锁释放完之后,才会去提交事务,所以,在释放锁和提交事务之间的过程,可能有其它线程进来,简单来讲,加的这个synchronized就是白加了。

如何解决?

方法1-新加一个方法,去调用加了@Transactional注解的方法

这个不用多说...

方法2-不使用synchronized,改为分布式锁

1.数据库锁实现

1.1数据库乐观锁实现
1.1.1基于字段version记录机制实现

乐观锁通常实现是基于数据库版本(version)的记录机制实现的。

比如:我简单假设一下,方便理解,有一个b_product产品表,这个产品表有个字段为count记录这个产品还有多少库存,当一个用户下单之后,此产品需要减一,如果做的不好的count_num出现负数就GG。所以在并发情况下,我肯定起码要保证count_num不能为负数,且一定要保证这段流程没有任何问题。

乐观锁的实现方式为在此b_product表中加上一个version字段,查询的时候将version也查出来,当更新的时候,version加1。所以在更新之前,需要将version查出来,更新的时候,如果version是相同的,就进行更新,否则就说明在这段期间有其它线程对它进行了操作,就不进行操作,然后就是你自己定义的逻辑处理了。

sql语句:

# 1.将此商品的version查出来
select version from t_product where id = #{id}
# 2.更新商品的信息(version与前面查出的相同才进行更新)
update t_product set count_num = count_num - 1 where id = #{id} and version = #{version}
1.1.2基于时间戳实现

其实和字段version一样的,同样的,需要在表中新增一个字段,字段类型使用时间戳timestamp,假设为update_time,和前面version实现方式一样,也是在提交更新的时候,将第一次查询的时间戳与更新前取到的时间戳进行对比,如果一致,说明没线程在这期间对它进行修改,如果不一致,说明这期间有线程对它进行了修改,就不更新,然后就是你自己定义的逻辑处理了。

1.2数据库悲观锁实现

这个稍微费那么一丢丢时间就是,实现方式就是,我认为别人一定会同时和我一起修改数据,我在操作数据的时候直接把数据锁住,直到我操作完成之后我才释放锁,因为上锁了其它人就不能修改数据了。

sql语句:

# 1.开始事务 (三种方式开始事务,选一种就行了)
begin
begin work
start transaction

# 2.查询出商品信息
# 获取锁
select name, count_num from b_product where id = #{id} for update

update t_product set count_num = count_num - 1 where id = #{id} and version = #{version}

# 3.提交事务(释放锁)
commit
commit work

当然,使用数据库的锁,必须要注意,不要处理不当,产生了死锁。

**死锁:**例如,如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。

2.基于Redis的分布式锁-单节点与集群

单节点
a6481c7e08171c5ace513f6e3e6e2e4c.png
Redis分布式锁的实现流程.png

RedisLock注解:

@Target(ElementType.METHOD)

RedisLockAspect进行AOP操作:

@Aspect

加锁释放锁的工具RedisLockHelper

@Component

Redis连接工具JedisUtil

@Component

配置文件以及配置:

redis:
@Setter

测试使用:

@RestController
@Service
为什么要使用UUID/当前线程ID作为Value?

不使用UUID/当前线程ID作为Value,存在的问题:

A线程启动,如果业务代码没有执行完毕,Key过期了,此时,B线程启动,因为A线程的Key已经过期了,所以B线程自然而然也得到了锁,最后B线程释放锁的时候,其实释放的是A线程的锁,这就导致了不安全的事情可能会发生。

---解决办法1

在加锁的时候,设置Value为UUID或者当前线程的ID当做Value,并在删除的时候判断对应的Value是不是自己线程的ID。也就是上面写的这种方式,请再次看我写的注释,就可以理解了。

/**
     * 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
     *
     * EX second :设置键的过期时间为 second 秒。SET key value EX second 效果等同于 SETEX key second value 。
     * PX millisecond :设置键的过期时间为 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
     * NX :只在键不存在时,才对键进行设置操作。SET key value NX 效果等同于 SETNX key value 。
     * XX :只在键已经存在时,才对键进行设置操作。
     *
     * @param key
     * @param value
     * @param timeout
     * @return 加锁是否成功
     */
---解决办法2

在加锁和释放锁的时候,设置一个守护线程,对key进行续命。

//开启守护线程:
Redis集群模式-Redis锁之Redlock算法

如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master挂掉,Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。

针对Redis集群架构,redis的作者antirez提出了Redlock算法,来实现集群架构下的分布式锁。

**Redlock算法:**现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:

  • 1.获取当前Unix时间,以毫秒为单位
  • 2.依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁 当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等
  • 3.客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
  • 4.如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要
  • 5.如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题
马丁博士对Redlock的质疑

假设多节点Redis系统有五个节点A/B/C/D/E和两个客户端C1和C2,如果其中一个Redis节点上的时钟向前跳跃会发生什么?

  • 客户端C1获得了对节点A、B、c的锁定,由于网络问题,法到达节点D和节点E
  • 节点C上的时钟向前跳,导致锁提前过期
  • 客户端C2在节点C、D、E上获得锁定,由于网络问题,无法到达A和B
  • 客户端C1和客户端C2现在都认为他们自己持有锁

最后马丁出了如下的结论:

  • 为了效率而使用分布式锁单Redis节点的锁方案就足够了Redlock则是个过重而昂贵的设计
  • 为了正确而使用分布式锁Redlock不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分

参考Demo,更多操作可以看Redlock的源码:

@Bean
@Autowired

推荐阅读文章:https://redis.io/topics/distlock

3.基于Zookeeper的分布式锁

主要原理就是:在Zookeeper创建一个临时的同步顺序节点,如果创建成功,说明获得了锁,如果创建失败,表示这歌锁已经被当前线程占有了,就对这个节点进行监听,当监听到这个节点被删除之后,就再进行创建临时节点。

说明:临时节点在ZK连接断开的时候会自动删除。

ac6be5ec9dee15d0df1d80eb5d8d16fc.png
Zookeeper分布式锁的实现流程.png

自定义锁的接口Lock

/**
 * 自定义锁接口
 */

锁的抽象方法:

public 

定义Zookeeper的连接,写为抽象类,让其锁的具体实现类继承:

public 

具体锁的实现:

public 

测试:

/**
 * Zookeeper实现分布式锁 - 方法2
 *
 * 使用临时顺序节点,临时顺序节点排序,每个临时顺序节点只监听它本身的前一个节点变化,
 * 避免产生“羊群效应”(一旦临时节点删除,释放锁,那么其他在监听这个节点变化的线程,就会去竞争锁,同时访问 ZooKeeper)。
 */

生成随机订单号:

/**
 * 生成随机订单号
 *
 * @author lzhpo
 */


点击阅读原文,可以访问我的个人博客~

相关文章:

  • amd关闭超线程_直接提高40帧?超线程开启/关闭游戏对比测试
  • easyui 控制某列显示不显示_称重显示控制器工作原理
  • python编写脚本教程_Python编写生成验证码的脚本的教程
  • python中图例legend标签内容_关于python 的legend图例,参数使用说明
  • 45个python入门案例_Python入门教程:15道不容错过的Python基础入门小案例
  • 初中学历python学不会_《差点学不会Python》——第二章 关于Python的一些基础知识...
  • python数据结构算法_python数据结构和算法
  • pythonfor循环语句例子_Python中的for循环语句
  • 乔布斯斯坦福大学演讲pdf_史蒂芬·保罗·乔布斯:2005斯坦福大学演讲【双语字幕】...
  • lua 去除小数点有效数字后面的0_Lua设计与实现--字符串篇
  • python贪吃蛇毕业设计_如何用Python写一个贪吃蛇AI
  • active mq topic消费后删除_面试官杠上消息队列?高可用、重复消费、丢失、顺序消息你懂吗?...
  • 天气预报c是什么意思_昨天“大雪”天气,对明年气候有什么影响?
  • 当退出python时是否释放全部内存_Python跑循环时内存泄露的解决方法
  • 为什么parsefloat加出来还是字符串_为什么股票资金流出了1000万,却还是封住了涨停板?知道套路的我眼泪都掉出来了...
  • 5、React组件事件详解
  • 78. Subsets
  • Java 实战开发之spring、logback配置及chrome开发神器(六)
  • JS创建对象模式及其对象原型链探究(一):Object模式
  • leetcode46 Permutation 排列组合
  • MySQL-事务管理(基础)
  • scrapy学习之路4(itemloder的使用)
  • 给第三方使用接口的 URL 签名实现
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 坑!为什么View.startAnimation不起作用?
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 区块链将重新定义世界
  • 如何解决微信端直接跳WAP端
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 我从编程教室毕业
  • 线性表及其算法(java实现)
  • 第二十章:异步和文件I/O.(二十三)
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • ​力扣解法汇总946-验证栈序列
  • ​软考-高级-系统架构设计师教程(清华第2版)【第12章 信息系统架构设计理论与实践(P420~465)-思维导图】​
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • ###C语言程序设计-----C语言学习(6)#
  • #Spring-boot高级
  • #vue3 实现前端下载excel文件模板功能
  • $GOPATH/go.mod exists but should not goland
  • (13)Hive调优——动态分区导致的小文件问题
  • (备忘)Java Map 遍历
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (一)为什么要选择C++
  • (原创)Stanford Machine Learning (by Andrew NG) --- (week 9) Anomaly DetectionRecommender Systems...
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • (转载)OpenStack Hacker养成指南
  • ./indexer: error while loading shared libraries: libmysqlclient.so.18: cannot open shared object fil
  • ./mysql.server: 没有那个文件或目录_Linux下安装MySQL出现“ls: /var/lib/mysql/*.pid: 没有那个文件或目录”...
  • .jks文件(JAVA KeyStore)
  • .NET Compact Framework 多线程环境下的UI异步刷新
  • .net MySql
  • .NET WebClient 类下载部分文件会错误?可能是解压缩的锅
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!