分布式锁实现方案1、基于Redis的SETNX操作实现的分布式锁

很多前辈们都写过类似的伪代码,但是前辈们都没有告诉你,redis分布式事务锁无论如何写都会存在一些问题(代码注释会给你揭晓),所以一定要活学活用(结合业务场景合理设置超时时间)

下面给出了一个相当来说实现算是完美一些(为什么是相对呢,因为无论如何实现都会存在)的锁代码,供大家学习与参考,关键代码也都给出了注释,如有疑问或错误,欢迎留言指正,谢谢。

调用示例:

final RedisDistributedLockTest test = new RedisDistributedLockTest();

for (int i = 0; i < 100; i++) {
    new Thread() {
        @Override
        public void run() {
            RedisDistributedLock lock = new RedisDistributedLock(redis, "orderId", 5000);
            try {
                lock.lock(10000, TimeUnit.SECONDS);
                test.doSth();

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }.start();
}     

源码如下:

package com.jd.o2o.lzc.lock;

import com.jd.jim.cli.Cluster;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * 
 * 基于Redis的SETNX操作实现的分布式锁
 * 
* @author [email protected] * */
public class RedisDistributedLock { private Cluster redis; // 锁的名字 private String lockKey; // 锁的值 private String lockVal = ""; // 默认锁的有效时长(毫秒) private long lockExpires; private boolean locked; // 当前jvm内持有该锁的线程(if have one) private Thread exclusiveOwnerThread; /** * * @param redis * @param lockKey lockKey * @param lockExpires lockKey过期时间,单位:毫秒 * @throws IOException */ public RedisDistributedLock(Cluster redis, String lockKey, long lockExpires){ this.redis = redis; this.lockKey = lockKey; this.lockExpires = lockExpires; } /** * 阻塞式获取锁 ,不过有超时时间,超过了tryGetLockTime还未获取到锁将直接返回false * @param tryGetLockTime * @param tryGetLockUnit * @return * @throws InterruptedException */ protected boolean lock(long tryGetLockTime, TimeUnit tryGetLockUnit){ try { // 超时控制 的时间可以从本地获取, 因为这个和锁超时没有关系, 只是一段时间区间的控制 long start = System.currentTimeMillis(); long timeout = tryGetLockUnit.toMillis(tryGetLockTime); // if !useTimeout, then it's useless //int tryTimes=0; while (System.currentTimeMillis() - start < timeout) { //tryTimes++; long lockExpireTime = System.currentTimeMillis() + lockExpires + 1;//锁超时时间 String stringOfLockExpireTime = String.valueOf(lockExpireTime); if (setnx(lockKey, stringOfLockExpireTime)) { // 获取到锁 // 成功获取到锁, 设置相关标识 locked = true; exclusiveOwnerThread = Thread.currentThread(); //System.out.println("拿到锁了,哈哈:"+tryTimes); return true; } //说明未获取到锁,进一步检查锁是否已经超时 String lockVal=redis.get(lockKey); //是存在lockVal=null的情况的,C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。 // C2通过SETNX向lockKey设置时间戳T0 发现有客户端已经获取锁,进入GET操作。 // 这时候C1客户端DEL掉锁成功。 // C2向lockKey发送GET命令,获取返回值T1(null)。 if(lockVal!=null&&Long.parseLong(lockVal)//表明已经超时了,原来的线程可能可能出现意外未能及时释放锁 String oldLockVal=redis.getSet(lockKey,stringOfLockExpireTime); //为什么会有下面这个判断呢?因为多线程情况下可能同时有多个线程在这一时刻发现锁过期,那么就会同时执行getSet获取锁操作, //通过下面的比较,可以找到第一个执行getSet操作的线程,让其获得锁,其它的线程则重试 //oldLockVal也存在null的情况,大家可以想想为什么 if(lockVal.equals(oldLockVal)){ redis.expire(lockKey, lockExpires, TimeUnit.MILLISECONDS); // 成功获取到锁, 设置相关标识 locked = true; exclusiveOwnerThread = Thread.currentThread(); return true; } } Thread.sleep(5L); } } catch (InterruptedException e) { e.printStackTrace(); } return false; } /** * 非阻塞,立即返回是否获取到锁 * @return */ public boolean tryLock() { if (setnx(lockKey, lockVal)) { // 获取到锁 // 成功获取到锁, 设置相关标识 locked = true; //setExclusiveOwnerThread(Thread.currentThread()); exclusiveOwnerThread = Thread.currentThread(); return true; } return false; } private boolean setnx(String lockKey, Object val) { if (redis.setNX(lockKey, String.valueOf(val))) { redis.expire(lockKey, lockExpires, TimeUnit.MILLISECONDS); return true; } return false; } public boolean isLocked() { return locked; } /** * 释放锁 */ public void unlock() { // 检查当前线程是否持有锁 if (Thread.currentThread() != exclusiveOwnerThread) { // 表明锁并非当前线程所持有,不应该由当前线程来释放锁 System.out.println("表明锁并非当前线程所持有,不应该由当前线程来释放锁exclusiveOwnerThread:" + exclusiveOwnerThread + ",Thread.currentThread():"+Thread.currentThread()+",lockKey" + lockKey); return; } //gaohongtianluck 忽略了一个地方。用del命令释放锁,如果线程A获得锁之后运行太久,久到另已经获得的锁失效了。 // 这时线程B进来,取缔了A上的锁,线程B运行到一半的时候,这时线程A也运行完了,杀一个回马枪把原本以为获取到的锁给del, // 实际上是B获得的锁,那么就会导致其他线程进来竞争,而B还以为自己独占锁 //回复Ffadsfoadfjaodjfalkd:我也在思考这个问题,我觉得有一种写法可以尽量避免。在锁的时候,如果锁住了,回传超时时间,作为解锁时候的凭证,解锁时传入锁的键值和凭证。我思考的解锁时候有两种写法: //1、解锁前get一下键值的value,判断是不是和自己的凭证一样。但这样存在一些问题: //1)get时返回null的可能,此时表示有别的线程拿到锁并用完释放 //2)get返回非null,但是不等于自身凭证。由于有getset那一步,当两个竞争线程都在这个过程中时,存在持有锁的线程凭证不等于value,而value是稍慢那一步线程设置的value。 // //2、解锁前用凭证判断锁是否已经超时,如果没有超时,直接删除;如果超时,等着锁自动过期就好,免得误删别人的锁。但这种写法同样存在问题,由于线程调度的不确定性,判断到删除之间可能过去很久,并不是绝对意义上的正确解锁。 // //关于解锁我只想到这么多,希望有帮助,欢迎拍砖多交流。 //综上所述,lzc.java实现采用了非常简单的方法,如上所述,即超时的情况下可能会出现误释放锁的场景,所以使用的时候就需要合理设置超时时间了 redis.del(lockKey); exclusiveOwnerThread = null; } }

你可能感兴趣的:(java,系统设计,锁)