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;
}
/**
* 尝试加锁
* <p>
* 尝试加锁的过程将会一直阻塞下去,直到加锁成功或超时
*
* @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