使用ELock实现高性能分布式锁(非轮询)
前言:
随着笔者的颜值不断提高,用户量的日益增长,传统的单机方案已经不能满足产品的需求。笔者在网上寻遍方案,发现均为人云亦云,一份以毫秒为精度的轮询分布式锁被转发转载上万次。然,该方案没法满足笔者性能要求。故此,笔者研发ELock插件,并发布本文章。
其实集群也好,分布式服务也好。当我们不能保证团队成员的整体素质,那么在某些业务上,分布式锁自然没法避免。
公认开发原则:能不使用分布式锁的,尽可能不使用
举个例子,一个商品交易,需要检查库存、检查余额、扣库存、扣款、变更订单状态。可能很多人觉得,在分布式环境下一定要分布式锁才能安全。
致此,笔者提供一种简单的方案:
订单处理{ if(订单状态!=待支付){ return 该订单已处理; } if(库存不足){ return 库存不足; } if(余额不足){ return 余额不足; } 事务管理(rollbackFor = Exception.class){ //修改订单状态 int changeLine = 执行语句( update 订单表 set status=已支付 where status=待支付 and orderId=订单号); if(changeLine < 1){ return 该订单已处理; } //扣库存 changeLine = 执行语句(update 商品表 set 库存=库存-订单信息.购买数量 where 库存>订单信息.购买数量 and 商品ID = ?); if(changeLine != 1){ throw CustomRuntimeException("库存不足"); } //扣款 changeLine = 执行语句(update 用户余额表 set 余额=余额-订单信息.订单金额 where 余额 > 订单信息.订单金额 and 订单信息.订单金额 > 0 and 用户ID = ?); if(changeLine != 1){ throw CustomRuntimeException("余额不足"); } } }
我们仔细来分析一下如上的整个逻辑
1、当一个业务进入逻辑体,先检查订单状态、余额和库存,不满足条件则返回错误(可阻挡非并发情况下的大部分业务流入事物)
2、进入事物后,先变更订单状态,如果变更失败,直接返回错误
3、当订单状态变更成功,则扣取库存,扣取库存失败必须抛出异常,让第二步的订单状态回滚。
4、扣取库存后,则进行扣款,当扣款失败,则抛出异常(由于在业务体走到这里,已经扣取了库存,本处不能return,需抛出异常,让事物回滚)
特别注意:语句中,通过where来进行余额不足和库存不足的条件判断。通过执行语句返回的影响行数,来判断是否扣取成功。 在以上流程中,我们发现,即便不使用分布式锁,也无并发问题。
===========================================================
以上介绍了在常见的业务中如何规避分布式锁,下面介绍一下笔者的高性能分布式锁
友情提示:切勿觉得笔者以上理论是拆自己的台,笔者作为互联网技术人,希望各位技术人能够将产品质量做到最好,少加班,多回家陪陪家人
ELock介绍
ELock是笔者闲暇之余写的一套分布式锁插件,代码非常精简、并且以非轮询阻塞的方式进行加锁控制。适用于面向用户的互联网产品,目前用在一套用户量为7位数的直播系统中。 源码地址:https://gitee.com/coodyer/Coody-Framework/tree/original/coody-elock
Maven引用代码(可关注更新情况):
<dependency> <groupId>org.coody.framework</groupId> <artifactId>coody-elock</artifactId> <!--更新于2019-01-22 10:23:00 --> <version>alpha-1.2.4</version> </dependency>
-
初始化JedisPool
//直接传入连接池初始化(注:无密码请传null) new ELockCache().initJedisPool(JediPool); //传入ip、端口、密码、超时时间初始化 new ELockCache().initJedisPool(host, port, secretKey, timeOut); //传入ip、端口、密码、超时时间、配置器初始化 new ELockCache().initJedisPool(host, port, secretKey, timeOut, jedisPoolConfig);
-
加锁
ELocker.lock(key, expireSecond);
-
释放锁
ELocker.unLock(key);
注意: 加锁代码(ELocker.lock(key, expireSecond);)。需try{}catch{}包围,并在finally释放锁(ELocker.unLock(key);)
try { ELocker.lock(key, 100); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getId() + ">>" + i); Thread.sleep(100l); } } catch (InterruptedException e) { e.printStackTrace(); } finally { ELocker.unLock(key); }
6. 测试代码
import java.util.ArrayList; import java.util.List; import org.coody.framework.elock.ELocker; import org.coody.framework.elock.redis.ELockCache; /** * 分布式锁测试 * @author Coody * */ public class ELockTest { //要加锁的key static String key = "TESTLOCK_1"; static { //初始化jedis连接 new ELockCache().initJedisPool("127.0.0.1", 16379, "123456", 10000); } public static void main(String[] args) { List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { [@Override](https://my.oschina.net/u/1162528) public void run() { test(); } }); threads.add(thread); } //启动十个线程 for (Thread thread : threads) { thread.start(); } } //要锁的方法 private static void test() { try { ELocker.lock(key, 100); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getId() + ">>" + i); Thread.sleep(100l); } } catch (InterruptedException e) { e.printStackTrace(); } finally { ELocker.unLock(key); } } }
执行效果:
===========================================================
以上介绍了这套锁的基本使用,下面开始介绍一下这套锁在Spring下的花样玩法
1、配置分布式锁中使用的缓存
<bean id="eLockCache" class="org.coody.framework.elock.redis.ELockCache" lazy-init="false"> <property name="jedisPool" ref="jedisPool" /> </bean>
2、配置分布式锁切面
<!-- 配置切面的bean --> <bean id="eLockInterceptor" class="org.coody.framework.elock.aspect.ELockAspect"></bean> <!-- 配置AOP --> <aop:config> <!-- 配置切面表达式 --> <aop:pointcut expression="@annotation(org.coody.framework.elock.annotation.ELock)" id="eLockPointcut" /> <!-- 配置切面和通知 order:越小优先级越高 --> <aop:aspect id="logAspect" ref="eLockInterceptor"> <aop:around method="rdLockForAspectj" pointcut-ref="eLockPointcut" /> </aop:aspect> </aop:config>
致此,分布式锁配置完成,开始进入我们的花样玩法。
NO1. 使用注解添加分布式锁:
@ELock(name = "USER_MODIFY_LOCK", fields = "userId", waitTime = 20) public void delUser(String userId) { userDao.delUser(userId); }
在ELock注解中,name代表key名字,field代表拼接的字段。
当fields所有字段长度超过32时,elock将会对key进行md5获取摘要作为缓存的key,即name:key。
本处fields支持选择对象的字段,即:方法参数名.字段值(如:userInfo.userId)
本处fields支持多个字段,fields={"userInfo.userId","orderInfo.orderId"}
当不指定key时,elock将会根据包名、类名、方法名和方法参数生成key
当不指定fields时,elock不会拼接任何多余参数,则该方法变成全局同步方法
如图:
NO2. 使用锁执行器添加分布式锁
public void delUser(String userId) throws InterruptedException { String key="USER_MODIFY_LOCK"+userId; Integer code=new AbstractLockAble(key,20) { @Override public Object doService() { return userDao.delUser(userId); } }.invoke(); }
通过 返回值=new AbstractLockAble(锁名称,超时时间){}.invoke()的方式,覆盖doService方法,将需要加锁的代码块放置doService方法里面执行。
如图:
===========================================================
以上介绍了通过注解进行加锁和通过执行器进行加锁的操作,如果在项目中觉得两种方式不可取,可采用上文中常规方式。
本处介绍下这套锁为何高性能。
笔者曾经百度搜索Java分布式锁实现,发现所提供方案都如出一辙(由于没有作图工具,就随便写下流程)。
1、尝试获得锁
2、死循环轮询获得锁
3、执行业务
4、释放锁
在网上查到的方案,相信很多小朋友都知道,不知道是谁通过这种方式来做分布式锁,然后被一大堆网友转载。
这种方案是可以实现锁,但是不适用于对外的互联网产品。
重大问题地雷:当多个线程尝试获得锁,只有一个线程会执行,剩下的线程都在轮询获得锁。这里我们假设时间精度为1ms,那就意味着每个线程每秒钟最多轮询1000次。然而在分布式锁中,我们需要借助中介容器去进行尝试获得锁的操作,如redis zookeeper。故此,我们假设这个key有100个线程,第一个线程执行卡住,那么,1个线程在执行业务,99个线程在以每秒钟1000的频次对中间容器发起ddos攻击。故此,如上方案不适用于对外的互联网产品。
介绍下笔者的方案:
1、尝试获得锁
2、线程入列并暂停
3、执行业务
4、发送消息释放锁,并唤醒下一个线程(轮询至第1步)
我们知道,redis也好,zookeeper也好,都有消息订阅机制。当业务流入的时候,获取锁失败的线程,都进入了挂起的状态,那么此时有一个线程在执行。当这个线程执行完毕后,发送消息,这时候所有的应用程序都收到了这个消息,并尝试获得锁,以此往复,实现业务体执行权限
作者:Coody
版权:©2014-2020 Test404 All right reserved. 版权所有
反馈邮箱:644556636@qq.com
问题反馈群:Java泛太平洋研究中心 218481849
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
漫画:奇怪,为什么在Java中 2*(i*i) 比 2*i*i 快?
既然我设计的两只小萌宠出场了,也该它们的粑粑出场了,有同学估计最近有点疑问,东哥为什么很长一段时间不输出原创文章了,难道不准备写下去了吗?我可是东哥的忠实读者呢! 东哥在这里告诉各位读者大大,东哥一定会写下去,除非真的有一天写不动了为止,东哥一直对技术痴迷,所以公众号还是会以技术文输出为主。 今天这篇文章,我们通过一个故事来深入聊聊 Java 编译背后的秘密。 东哥说这段代码来自于 Stackoverflow(关于这个网站,东哥似乎分享了无数次《这三个网站的使用技巧,你值得收藏》,真正去逛这个网站的同学还是很少),如下。 public static void main(String[] args) { long startTime = System.nanoTime(); int n = 0; for (int i = 0; i < 1000000000; i++) { n += 2 * i * i; } System.out.println((double) (System.nanoTime() - startTime) / 1000000000 + " s"); System...
- 下一篇
基于shiro的按钮级别的权限管理系统
一、项目背景 作为程序猿的你,是否在大学课堂上听到老师讲权限管理一脸懵逼;是否在互联网上看到炫酷的权限管理系统一脸羡慕;是否在公司学习使用权限管理一脸激动。那么,今天你看到这个教程之后,请你不要再懵逼,请不要再羡慕,请肆无忌惮的激动吧。好嗨哟,即将带你走上人生的巅峰。下面将手把手的教你实现基于shiro权限框架的权限管理系统,真正意义上的在按钮级别上完成权限控制。注:本教程侧重点在于shiro框架的使用与权限控制逻辑的实现,对于其它知识点不在重点讨论范围内。如需学习更多关于shiro的知识可在我的文章查看。 二、技术栈 后台:spring+springmvc+mybatis+shiro+kaptcha+fastjson+log4j+druid+maven+echcache+pageHelper等 前端:vue+jquery+iview+ztree等 数据库:mysql 其它:IntelliJ IDEA 2018.2.4 x64+tomcat8.0+jdk1.8 三、项目结构 四、项目预览 图1 登录首页 图2 系统首页 图3 用户列表 图4 用户新增 图5 角色列表 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程