java分布式锁工具类_java 通过redis实现分布式锁

1. 开局

在多线程环境中,经常会碰到需要加锁的情况,由于现在的系统基本都是集群分布式部署,JVM的lock已经不能满足分布式要求,分布式锁就这样产生了。。。

百度一下,网上有很多分布式锁的方案或者例子,琳琅满目,看了之后不知所措,总体来说有以下几种:

基于数据库

基于zookeeper

基于redis

基于memcached

各有优缺点和实现难度,这里就不一一分析。本文主要是基于redis的setnx实现分布式锁,比较简单有一定的局限性,欢迎大家提出意见建议!

2. 加锁过程

执行redis的setnx,只有key不存在才能set成功(实际使用的是set(key, value, "NX", "EX", seconds),redis较新版本支持)

如果set成功(同时也设置了key的过期时间),则表示加锁成功

如果set失败,则每次sleep(x)毫秒后不断尝试,直到成功或者超时

3. 释放过程

判断加锁是否成功

如果成功,则执行redis的del删除

4. 问题思考

加锁时,锁的redis key过期时间多长合适?

需要根据业务执行的时间长度来评估,默认30秒满足绝大部分需求,支持动态修改

加锁时,重试超时时间多长合适?本文设置的是过期时间的1.2倍,目的是在最坏的情况下等待锁过期后,尽量保证获取到锁,否则抛出超时异常。这个设置不完全合理

加锁时,重试的sleep时间多长合适?本文采用的是随机[50-300)毫秒,避免出现大量线程同时竞争,目的是错峰吧

释放时,如何避免释放了其他线程的锁(A获取锁后由于挂起导致锁到期自动释放;此时B获取到锁,而A又恢复运行释放了B的锁)?在初始化锁时生个一个唯一字符串,作为redis锁的value;value一致时表明是自己的锁,可以释放

5. 上代码!

用法

RedisLock lock = new RedisLock(redisHelper, lockKey);

try {

// 执行加锁,防止并发问题

lock.tryLock();

// do somethings

doSomethings()

}

finally {

// 释放锁

lock.release();

}

RedisLock实现(注:依赖RedisHepler类,仅仅是对jedis的一层封装,可自行实现)

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

/**

* RedisLock

*

* @version 2017-9-21上午11:56:27

* @author xiaoyun.zeng

*/

public class RedisLock {

private Logger logger = LoggerFactory.getLogger(getClass());

/**

* key前缀

*/

private static final String PREFIX = "lock:";

/**

* 操作redis的工具类

*/

private RedisHelper redisHelper;

/**

* redis key

*/

private String redisKey = null;

/**

* redis value

*/

private String redisValue = null;

/**

* 锁的过期时间(秒),默认30秒,防止线程获取锁后挂掉无法释放锁

*/

private int lockExpire = 30;

/**

* 尝试加锁超时时间(毫秒),默认为expire的1.2倍

*/

private int tryTimeout = lockExpire * 1200;

/**

* 尝试加锁次数计数器

*/

private long tryCounter = 0;

/**

* 加锁成功标记

*/

private boolean success = false;

private long startMillis = 0;

private long expendMillis = 0;

/**

* RedisLock

*

* @param redisHelper

* @param lockKey

*/

public RedisLock(RedisHelper redisHelper, String lockKey) {

this.redisHelper = redisHelper;

this.redisKey = PREFIX + lockKey;

// 生成redis value,用于释放锁时比对是否属于自己的锁

// 生成规则 lockKey+时间戳+随机数,避免重复

// 乐观地认为:

// 1、同一毫秒内,随机数相同的概率极小

// 2、释放非自己线程锁的几率极小(release方法有说明这种情况)

this.redisValue = lockKey + "-" + System.currentTimeMillis() + "-" + this.random(10000);

}

/**

* RedisLock

*

* @param redisHelper

* @param lockKey

* @param expire

*/

public RedisLock(RedisHelper redisHelper, String lockKey, int lockExpire) {

this(redisHelper, lockKey);

// 过期时间

this.lockExpire = lockExpire;

// 超时时间(毫秒),默认为expire的1.2倍

this.tryTimeout = lockExpire * 1200;

}

/**

* 尝试加锁

*

* 尝试加锁的过程将会一直阻塞下去,直到加锁成功或超时

*

* @version 2017-9-21下午12:00:07

* @author xiaoyun.zeng

* @return

*/

public void tryLock() throws RuntimeException {

startMillis = System.currentTimeMillis();

// 首次直接请求加锁

if (!lock()) {

do {

// 超时判断,避免永远获取不到锁的情况下,一直尝试

// 超时抛出runtime异常

if (System.currentTimeMillis() - startMillis >= tryTimeout) {

throw new RuntimeException("尝试加锁超时" + tryTimeout + "ms");

}

// 随机休眠[50-300)毫秒

// 避免出现大量线程同时竞争

try {

Thread.sleep(this.random(250) + 50);

}

catch (InterruptedException e) {

// 出现异常直接抛出

throw new RuntimeException(e);

}

}

while (!lock());

}

}

/**

* 释放锁

*

* @version 2017-9-21下午12:00:21

* @author xiaoyun.zeng

* @param lockKey

*/

public void release() {

// 加锁成功才执行释放

if (success) {

// 释放前,检查redis value是否一致

// 避免A获取锁后由于挂起导致锁到期自动释放

// 此时B获取到锁,而A又恢复运行释放了B的锁

String value = redisHelper.get(redisKey);

if (redisValue.equals(value)) {

redisHelper.del(redisKey);

logger.debug("已释放锁:{}", redisValue);

}

}

}

/**

* 加锁

*

* @version 2017-9-21下午6:25:58

* @author xiaoyun.zeng

* @param key

* @param value

* @param lockExpire

* @return

*/

private boolean lock() {

// 加锁计数器+1

tryCounter++;

// 调用redis setnx完成加锁,返回true表示加锁成功,否则失败

success = redisHelper.setNx(redisKey, redisValue, lockExpire);

// 计算总耗时

expendMillis = System.currentTimeMillis() - startMillis;

// 记录日志

if (success) {

logger.debug("加锁成功:尝试{}次,耗时{}ms,{}", tryCounter, expendMillis, redisValue);

}

return success;

}

/**

* 产生随机数

*

* @version 2017-9-22上午10:05:52

* @author xiaoyun.zeng

* @param max

* @return

*/

private int random(int max) {

return (int) (Math.random() * max);

}

}

6. 测试代码

单元测试:

@RunWith(SpringRunner.class)

@SpringBootTest

public class RedisLockTest {

@Autowired

private RedisHelper redisHelper;

@Test

public void test() {

for (int i = 0; i < 10; i++) {

new Thread(new Runnable() {

@Override

public void run() {

RedisLock lock = new RedisLock(redisHelper, "zxy");

try {

lock.tryLock();

try {

Thread.sleep(2 * 1000);

}

catch (InterruptedException e) {

e.printStackTrace();

}

}

finally {

lock.release();

}

}

}).start();

}

while(true) {

}

}

}

日志输出:

2017/10/12 17:47:28.335 [Thread-8] DEBUG [RedisLock.161] 加锁成功:尝试1次,耗时4ms,zxy-1507801648330-6665

2017/10/12 17:47:30.340 [Thread-8] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648330-6665

2017/10/12 17:47:30.351 [Thread-14] DEBUG [RedisLock.161] 加锁成功:尝试12次,耗时2018ms,zxy-1507801648333-6866

2017/10/12 17:47:32.356 [Thread-14] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648333-6866

2017/10/12 17:47:32.396 [Thread-11] DEBUG [RedisLock.161] 加锁成功:尝试22次,耗时4065ms,zxy-1507801648331-5217

2017/10/12 17:47:34.400 [Thread-11] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648331-5217

2017/10/12 17:47:34.430 [Thread-12] DEBUG [RedisLock.161] 加锁成功:尝试39次,耗时6098ms,zxy-1507801648332-7708

2017/10/12 17:47:36.433 [Thread-12] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648332-7708

2017/10/12 17:47:36.453 [Thread-17] DEBUG [RedisLock.161] 加锁成功:尝试50次,耗时8119ms,zxy-1507801648334-2362

2017/10/12 17:47:38.457 [Thread-17] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648334-2362

2017/10/12 17:47:38.494 [Thread-9] DEBUG [RedisLock.161] 加锁成功:尝试57次,耗时10164ms,zxy-1507801648330-7086

2017/10/12 17:47:40.497 [Thread-9] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648330-7086

2017/10/12 17:47:40.587 [Thread-13] DEBUG [RedisLock.161] 加锁成功:尝试70次,耗时12254ms,zxy-1507801648333-8881

2017/10/12 17:47:42.590 [Thread-13] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648333-8881

2017/10/12 17:47:42.611 [Thread-15] DEBUG [RedisLock.161] 加锁成功:尝试82次,耗时14276ms,zxy-1507801648335-2509

2017/10/12 17:47:44.614 [Thread-15] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648335-2509

2017/10/12 17:47:44.699 [Thread-16] DEBUG [RedisLock.161] 加锁成功:尝试89次,耗时16365ms,zxy-1507801648334-5791

2017/10/12 17:47:46.702 [Thread-16] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648334-5791

2017/10/12 17:47:46.802 [Thread-10] DEBUG [RedisLock.161] 加锁成功:尝试106次,耗时18471ms,zxy-1507801648331-7347

2017/10/12 17:47:48.805 [Thread-10] DEBUG [RedisLock.137] 已释放锁:zxy-1507801648331-7347

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/442407.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java 的对象类用_java基础(第零篇)对象与类

前言&#xff1a;本文讲述java中对象与类的一些概念。包括对象与类的有关概念&#xff0c;类间五种关系&#xff0c;类的访问权限等。在java中&#xff0c;一切都可以用对象来描述&#xff0c;操作对象的标识符只不过是对象的一个引用&#xff0c;一个对象可以有多个引用&#…

buffer java nio_Java NIO深入理解Buffer(缓冲区)

前言Github&#xff1a;https://github.com/yihonglei/java-allProject&#xff1a;java-nio一 Buffer概述Java NIO中的Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区&#xff0c;从缓冲区写入到通道中的。缓冲区本质上是一块可以写入数据&#xff0c;然后可以从中读取…

java robot键值_Java:使用Robot类模拟键盘, 以Alt码方式输出汉字

java.awt.Robot类Java提供java.awt.Robot类来模拟操作键盘和鼠标, 下面是一个简单的demopublic static void keyPressByInt(Robot r,int key, int time){r.keyPress(key);r.keyRelease(key);if (time > 0) {r.delay(time);}}public static void main(String[] args) throws …

php设置上传文件大小限制_php修改上传文件大小限制实例详解

php修改上传文件大小限制实例详解php在默认情况下&#xff0c;只允许上传一定大小的文件&#xff0c;当上传文件大小超过这个值时&#xff0c;将会出错。本文章将向大家讲解php.ini中如何修改上传文件大小限制。需要修改的设置有三个地方&#xff0c;请看下文&#xff1a;1、 修…

java调用 火眼臻睛,火眼臻睛车牌识别SDK评测

【CPS中安网 cps.com.cn】CPS LAB总评:用专业角度解读产品--CPS评测中心对火眼臻睛车牌识别SDK进行了全面评测,火眼臻睛车牌识别SDK在综合识别率、车牌定位成功率、大角度下的识别率、夜间环境下的识别率、极端环境下的识别率、支持的最小车牌像素宽度等测试表现,都位于行业前列…

php 获取 uri,获取URI地址

前台和后台的URI路由地址&#xff1a;APP目录/控制器文件/方法函数会员中心的URI路由地址&#xff1a;member/APP目录/控制器文件/方法函数通过动态地址获取URL:index.php?sAPP目录&c控制器文件&m方法函数获取当控制器的URI地址&#xff1a;\Phpcmf\Service::L(Router…

java各层级限流对比,面试官说:来谈谈限流-从概念到实现,一问你就懵逼了?...

后端服务的接口都是有访问上限的&#xff0c;如果外部qps或并发量超过了访问上限会导致应用瘫痪。所以一般都会对接口调用加上限流保护&#xff0c;防止超出预期的请求导致系统故障。从限流类型来说一般来说分为两种:并发数限流和qps限流&#xff0c;并发数限流就是限制同一时刻…

php 生成验证码干扰元素,PHP生成指定位数验证码与可控干扰元素第二篇

这篇文章介绍的内容是关于PHP生成指定位数验证码与可控干扰元素第二篇&#xff0c;有着一定的参考价值&#xff0c;现在分享给大家&#xff0c;有需要的朋友可以参考一下生成验证码&#xff1a;文件名为&#xff1a;buildVerifyCode.func.php<?php //生成指定位数可控混合验…

mysql and 和where,关于mysql:连接sql查询中where和and子句的区别

本问题已经有最佳答案&#xff0c;请猛点这里访问。下面两个SQL查询有什么区别和号根据以下两个测试结果速度更快(237比460)。据我所知&#xff0c;这是一个标准。。氧化镁不&#xff0c;有细微的差别&#xff0c;你不能说没有差别除了语法之外没有别的区别。虽然只有一个简短的…

matlab里输出恒压的逆变器,基于IGBT逆变器的异步电机变频调速系统的MATLAB仿真...

异步电机变频调速系统电路仿真模型如图(4)所示。直流电压不621V&#xff0c;逆变器为IGBT 的三相半桥逆变器&#xff0c;电机为异步电机模块&#xff0c;其主电路由直流电压源、逆变器和电机依次相连。图(4)变频调速系统控制部分&#xff0c;利用“Step”模块设定频率指令f1*&a…

php 获取京东交易账号,PHP爬虫爬取京东列表

这里使用到了一个php插件下面是源码simple_html_dom.phpdefined(IN_ECS);define(HDOM_TYPE_ELEMENT, 1);define(HDOM_TYPE_COMMENT, 2);define(HDOM_TYPE_TEXT, 3);define(HDOM_TYPE_ENDTAG, 4);define(HDOM_TYPE_ROOT, 5);define(HDOM_TYPE_UNKNOWN, 6);define(HDOM_QUOTE_DOU…

星模php格,extend/base/TencentSms.php · 纵之格/ShopXO - Gitee.com

// ----------------------------------------------------------------------// | ShopXO 国内领先企业级B2C免费开源电商系统// ----------------------------------------------------------------------// | Copyright (c) 2011~2019 http://shopxo.net All rights reserve…

matlab中云模型,云模型简介与个人理解matlab程序.doc

.........................专业资料分享随着不确定性研究的深入&#xff0c;越来越多的科学家相信&#xff0c;不确定性是这个世界的魅力所在&#xff0c;只有不确定性本身才是确定的。在众多的不确定性中&#xff0c; HYPERLINK "/view/606111.htm" \t "_blank…

php dns刷新,Windows DNS缓存自动刷新

Windows DNS缓存自动刷新admin • 2018 年 09 月 04 日DNS(域名服务器)DNS(Domain Name Server)是进行域名和与之相对应的ip地址转换的服务器。DNS中保存了一张域名和与之相应的ip地址的表&#xff0c;以解析消息的域名。DNS轮训在统一主机添加多条A记录&#xff0c;这就是DNS轮…

php日期相减函数,倒计时函数_计算两个时间相差值_PHP函数

**PHP倒计时函数、求两个日期时间之间相差的时间函数、计算时差函数_PHP函数笔记**php/*** 求两个日期时间之间相差的时间* (针对1970年1月1日之后&#xff0c;求之前可以采用泰勒公式)* param string $day1 2016-10-01 10:01:08* param string $day2 2020-10-01 10:01:08* ret…

jq上传file到php,jQuery文件上传

插件描述&#xff1a;这是一个文件上传的展示&#xff0c;看很多插件&#xff0c;并不满意就自己写了一个&#xff0c;可能兼容性不是很好&#xff0c;没有准确进行测试过&#xff0c;还请各位不吝赐教更新时间&#xff1a;2019/3/25 下午8:53:17更新说明&#xff1a;修复上传成…

oracle导出建表主键,oracle主键自动生成 配合hibernate的生成策略详解

hibernate配合oracle自动生成主键策略有两种方法&#xff1a;A)设置ID的增长策略是sequence&#xff0c;同时指定sequence的名字&#xff0c;最好每个表建一个sequence&#xff0c;此种做法就如同MS-SQL,MY-SQL中的自动增长一样&#xff0c;不需要创建触发器&#xff0c;具体的…

oracle找到表的位置,查看Oracle表中的指定记录在数据文件中的位置

查看Oracle表中的指定记录位置select rowid,user_id from sshr.xx_user where user_id3010586select rowid,dbms_rowid.rowid_object(rowid) object_id,dbms_rowid.rowid_relative_fno(rowid) file_id,dbms_rowid.rowid_block_number(rowid) block_id,dbms_rowid.rowid_row_num…

oracle异常抛出,ORACLE 存储过程异常捕获并抛出

for tab_name in tables loopexecute immediate drop table ||tab_name; --此处可能会报错end loop;当前情况是&#xff0c;循环表&#xff0c;进行删除&#xff0c;如果出现表不存在&#xff0c;则会异常中断&#xff0c;导致整个存储过程挂掉&#xff0c;需求是要能跳过错误的…

oracle的主目录怎么删除,删除oracle数据库卸载

oracle 10g在win上卸载软件环境1 、Windows XPOracle 10g2、 Oracle安装路径为 d:\Oracle实现方法1 、开始—设置—控制面板—管理工具—服务停止所有 Oracle服务;Or acleUfiCQiisoXeorcl 名称 I细I好I启动类型I aat为ffetwork Frov-isi. 「, 巴SOrae le StrTice…