再谈使用java实现分布式锁-redis篇

当前网上可以找到许多基于redis使用java实现的分布式锁的代码,其主要实现方式主要有以下几种:

1.      SETNXGETSET、GET、DEL

加锁时,使用SETNX设置锁名和锁的到期时间,若设置成功则获取锁;否则再检查锁是否已过期,是则使用GETSET设置新的到期时间,设置成功则获取到锁,获取到锁后记一下状态;解锁时,若锁已过期则直接解锁,否则根据状态判断是否由自己执有锁,是则解锁。

2.      SETNXEXPIRE、GET、DEL

加锁时,使用SETNX设置锁名和锁的执有者,使用EXPIRE设置锁的过期时间;解锁时,先查询锁是否还由锁执有者执有,是则直接DEL解锁。

这两种实现方式在实际代码中都会各种各样的问题,上代码来分析:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import redis.clients.jedis.Jedis;

public class WrongARedisDLock implements DLock {
    private static final Logger LOG = LogManager.getLogger(WrongARedisDLock.class);
    private final String lockName;
    private final int lockDuration;
    private final String lockSubject;
    private final Jedis jedis;

    public WrongARedisDLock(String lockName, int lockDuration, String lockSubject, Jedis jedis) {
        this.lockName = lockName;
        this.lockDuration = lockDuration;
        this.lockSubject = lockSubject;
        this.jedis = jedis;
    }
    public void lock() {
        tryLock(Long.MAX_VALUE);
    }
    public boolean tryLock() {
        return tryLock(0);
    }
    public boolean tryLock(long waitTimeout) {
        long startTime = System.currentTimeMillis();
        while (true) {
            try {
                // 设置锁名和锁申请主体的唯一标识
                if (jedis.setnx(lockName, lockSubject) != null) {
                    // 若此时程序故障退出,锁将无法释放
                    jedis.expire(lockName, lockDuration);
                    return true;
                }
                String lockOwner = jedis.get(lockName);
                if (lockSubject.equals(lockOwner)) {
                    return true;
                }
                if (lockOwner == null) {
                    continue;
                }
                if (System.currentTimeMillis() - startTime < waitTimeout) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
                return false;
            } catch (Exception e) {
                LOG.error("tryLock error", e);
                if (System.currentTimeMillis() - startTime < waitTimeout) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
                return false;
            }
        }
    }

    public void unlock() {
        try {
            String lockOwner = jedis.get(lockName);
            if (lockSubject.equals(lockOwner)) {
                // 若此时锁过期了并且被其它申请者成功获取,则将误删锁
                jedis.del(lockName);
            }
        } catch (Exception e) {
            LOG.error("unlock error", e);
        }
    }
}
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import redis.clients.jedis.Jedis;

public class WrongBRedisDLock implements DLock {
    private static final Logger LOG = LogManager.getLogger(WrongBRedisDLock.class);
    private final String lockName;
    private final long lockDuration;
    private final String lockSubject;
    private final Jedis jedis;
    private boolean isLocked = false;

    public WrongBRedisDLock(String lockName, long lockDuration, String lockSubject, Jedis jedis) {
        this.lockName = lockName;
        this.lockDuration = lockDuration;
        this.lockSubject = lockSubject;
        this.jedis = jedis;
    }
    public void lock() {
        tryLock(Long.MAX_VALUE);
    }
    public boolean tryLock() {
        return tryLock(0);
    }
    public boolean tryLock(long waitTimeout) {
        long loopStartTime = System.currentTimeMillis();
        while (true) {
            try {
                long startTime = System.currentTimeMillis();
                String expireTimeStr = String.valueOf(startTime + lockDuration + 1);
                // 设置锁名和锁的到期时间,此处就存在多个不同进程或服务器当前时间可能会不一致的问题
                if (jedis.setnx(lockName, expireTimeStr) != null) {
                    isLocked = true;
                    return true;
                }
                // 获取锁的过期时间
                String oldExpireTimeStr = jedis.get(lockName);
                if (oldExpireTimeStr == null) {
                    continue;
                }
                long oldExpireTime = Long.parseLong(oldExpireTimeStr);
                if (startTime > oldExpireTime) {
                    // 在并发量较大时,可能会有多个锁申请主体同时进入到这里,并且都会修改锁的过期时间,
                    // 这样会造成锁的实际过期时间比锁执有者设置的时间靠后
                    oldExpireTimeStr = jedis.getSet(lockName, expireTimeStr);
                    if (oldExpireTimeStr != null) {
                        // 以下代码可以保证第一个申请成功的主体获取到锁
                        oldExpireTime = Long.parseLong(oldExpireTimeStr);
                        if (startTime > oldExpireTime) {
                            isLocked = true;
                            return true;
                        }
                    }
                }
                if (System.currentTimeMillis() - loopStartTime < waitTimeout) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
                return false;
            } catch (Exception e) {
                LOG.error("tryLock error", e);
                if (System.currentTimeMillis() - loopStartTime < waitTimeout) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
                return false;
            }
        }
    }
    public void unlock() {
        try {
            // 获取锁的到期时间
            String oldExpireTimeStr = jedis.get(lockName);
            if (oldExpireTimeStr != null) {
                long oldExpireTime = Long.parseLong(oldExpireTimeStr);
                if (oldExpireTime < System.currentTimeMillis()) {
                    // 若此时锁过期了并且被其它申请者成功获取,则将误删锁
                    jedis.del(lockName);
                } else {
                    if (isLocked) {
                        // 若此时锁过期了并且被其它申请者成功获取,则将误删锁
                        jedis.del(lockName);
                    }
                }
            }
            isLocked = false;
        } catch (Exception e) {
            LOG.error("unlock error", e);
        }
    }
}

正确的实现方式如下:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class RedisDLock implements DLock {
    private static final Logger LOG = LogManager.getLogger(RedisDLock.class);
    private final String lockName;
    private final long lockDuration;
    private final String lockSubject;
    private final Jedis jedis;

    public RedisDLock(String lockName, long lockDuration, String lockSubject, Jedis jedis) {
        this.lockName = lockName;
        this.lockDuration = lockDuration;
        this.lockSubject = lockSubject;
        this.jedis = jedis;
    }
    public void lock() {
        tryLock(Long.MAX_VALUE);
    }
    public boolean tryLock() {
        return tryLock(0);
    }
    public boolean tryLock(long waitTimeout) {
        long startTime = System.currentTimeMillis();
        while (true) {
            try {
                // 尝试加锁。等同于 SETNX + EXPIRE,通过设置一个属性值完成加锁过程
                if (jedis.set(lockName, //  锁名
                        lockSubject,  //  锁申请人
                        "NX",           //  锁名不存在时插入
                        "PX",           //  锁的有效时长的时间单位为millisecond
                        lockDuration    //  锁的有效时长
                ) != null) {
                    // 加锁成功
                    return true;
                }
                // 若加锁不成功,获取锁的拥有者。加锁不成功有可能是申请者已执有此锁,也可能是其他人执有此锁
                String lockOwner = jedis.get(lockName);
                // 若锁执有者与锁申请者相同,则返回申请者已占用此锁
                if (lockSubject.equals(lockOwner)) {
                    return true;
                }
                // 若锁未被占用,则再次申请锁
                if (lockOwner == null) {
                    continue;
                }
                if (System.currentTimeMillis() - startTime < waitTimeout) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
                // 返回锁已被其他申请主体占用
                return false;
            } catch (Exception e) {
                LOG.error("tryLock error", e);
                if (System.currentTimeMillis() - startTime < waitTimeout) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
                return false;
            }
        }
    }
    // 使用transaction解锁
    public void unlock1() {
        try {
            jedis.watch(lockName); //  添加对锁的监控,若锁的相关属性发生变化,则整个事务将不执行
            String lockOwner = jedis.get(lockName);
            if (lockSubject.equals(lockOwner)) {
                // 若锁未超时,则del操作会删除锁记录
                // 若锁已超时,当前锁无执有者,则del操作不会造成影响
                // 若锁已超时,当前锁被其他人执有,由于watch监控到锁的属性有变化,则事务中的del操作不会真正执行
                Transaction tx = jedis.multi();
                tx.del(lockName);
                tx.exec();
            } else {
                jedis.unwatch(); // 若不再执有锁,则放开监控
            }
        } catch (Exception e) {
            LOG.error("unlock error", e);
        }
    }
    // 使用lua script解锁
    public void unlock() {
        try {
            String script =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
            jedis.eval(script,Collections.singletonList(lockName),Collections.singletonList(lockSubject));
        } catch (Exception e) {
            LOG.error("unlock error", e);
        }
    }
}



猜你喜欢

转载自blog.csdn.net/netyeaxi/article/details/79819640