Redis分布式锁实现指北

背景

分布式锁在业务场景中的应用十分广泛,当你的服务部署在分布式的多节点集群中,在执行某一个方法时,需要控制并发的情况,保证同一时间只有一个请求可以执行,你一定需要分布式锁来实现。

最常见的分布式锁,一般就是Redis和ZK(亦或者MySQL的for update也可以充当),当然也有很多其他的实现,本篇的主角,就是Redis,我们来逐步使用实现一个简单易用的Redis分布式锁。

RedisLock

首先,我们先来回忆一下Java的Lock是怎么玩的:

ReentrantLock lock = new ReentrantLock();
try {
   
    lock.lock();
    // do something...
} finally {
   
    lock.unlock();
}

很简单,很优雅,那么我们就仿照Java的API来实现我们的Redis Lock。

最直观的实现方式,就是使用Redis String的setnx方法,如果key存在就放弃,如果key不存在就set一个值,并指定过期时间,释放锁时,就把key删除,非常easy。

Version1:

@Component
public class RedisLockUtil {
   
  	@Autowired
    private JedisClient jedisClient;

    public boolean lock(String key, long lockTimeout) {
   
        key = key + ".lock";
        UUID uuid = UUID.randomUUID();
        String res = jedisClient.set(key, uuid.toString(), "nx", "px", lockTimeout);
        if (Objects.equals("OK", res)) {
   
            return true;
        }
        return false;
    }

    public boolean unlock(String key) {
   
        key = key + ".lock";
        Long res = jedisClient.del(key);
        return Objects.equals(res, 1L);
    }
}  

Version1的实现非常简单,也可以满足基本的需求,但是有一个问题,任何线程,只要知道lockkey的名称,都可以调用unlock方法,这显然不是我们所希望的,所以我们在此基础上,进行一点优化,只有加锁的人,才可以释放锁,因此,我们需要保存一些额外的信息。

Version2:

我们新建一个Bean,存储RedisLock的相关信息,改造后我们的实现如下:

public class RedisLock {
   
    private String key;
    private final UUID uuid;
    private long lockTimeout;
    private long startLockTimeMillis;
    private long getLockTimeMillis;

    public RedisLock(String key, UUID uuid, long lockTimeout, long startLockTimeMillis, long getLockTimeMillis) {
   
        this.key = key;
        this.uuid = uuid;
        this.lockTimeout = lockTimeout;
        this.startLockTimeMillis = startLockTimeMillis;
        this.getLockTimeMillis = getLockTimeMillis;
    }

    public String getKey() {
   
        return key;
    }

    public void setKey(String key) {
   
        this.key = key;
    }

    public UUID getUuid() {
   
        return uuid;
    }

    public long getLockTimeout() {
   
        return lockTimeout;
    }

    public void setLockTimeout(long lockTimeout) {
   
        this.lockTimeout = lockTimeout;
    }

    public long getGetLockTimeMillis() {
   
        return getLockTimeMillis;
    }

    public void setGetLockTimeMillis(long getLockTimeMillis) {
   
        this.getLockTimeMillis = getLockTimeMillis;
    }

    public long getStartLockTimeMillis() {
   
        return startLockTimeMillis;
    }

    public void setStartLockTimeMillis(long startLockTimeMillis) {
   
        this.startLockTimeMillis = startLockTimeMillis;
    }
}
@Component
public class RedisLockUtil {
   

    @Autowired
    private JedisClient jedisClient;

    // 释放锁的Lua脚本
    private static final String LUA_SCRIPT_UNLOCK =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "    redis.call('del', KEYS[1]); " +
                    "    return 'suc' " +
                    "else " +
                    "    return 'fail' " +
                    "end";

    public RedisLock lock(String key, long lockTimeout) {
   
        key = key + ".lock";
        UUID uuid = UUID.randomUUID();
        String res = jedisClient.set(key, uuid.toString(), "nx", "px", lockTimeout);
        if (Objects.equals("OK", res)) {
   
            return new RedisLock(key, uuid, lockTimeout, System.currentTimeMillis());
        }
        return null;
    }

    public boolean unlock(RedisLock lock) {
   
        if (lock == null) {
   
            return false;
        }

        try {
   
            String lockValue = lock.getUuid().toString();
            JedisClusterPipeline pipeline = jedisClient.getPipelined();
            Response<String> response = pipeline.eval(LUA_SCRIPT_UNLOCK,
                    Collections.singletonList(lock.getKey()),
                    Collections.singletonList(lockValue)
            );
            pipeline.sync();
            return response != null && Objects.equals(response.get(), "suc");
        } catch (Exception e) {
   
            return false;
        }
    }
}

Well,改造后的方法,好像比Version1复杂了一些,别着急,其实并不复杂,为了保证原子性,删除lockkey部分我们采用了Lua脚本的方式实现,只有当lockkey的value校验一致时,才可以对key进行删除,这样就解决了Version1中出现的问题。

Version2基本可以运行的很好了,但是还有一个问题,我们知道Java的synchronized和ReentrantLock的实现,都是带有自旋抢锁功能的,当第一次没有获取到锁后,会尝试自旋等待,再次尝试抢锁,而我们的实现,好像并不具备这个功能,这怎么可以呢?那我们也要加入自旋。

Version3:

在RedisLock Bean中加入一些额外的信息:

public class RedisLock {
   
    private String key;
    private final UUID uuid;
    private long lockTimeout;
    private long startLockTimeMillis;
    private long getLockTimeMillis;
    private int tryCount;

    public RedisLock(String key, UUID uuid, long lockTimeout, long startLockTimeMillis, long getLockTimeMillis, int tryCount) {
   
        this.key = key;
        this.uuid = uuid;
        this.lockTimeout = lockTimeout;
        this.startLockTimeMillis = startLockTimeMillis;
        this.getLockTimeMillis = getLockTimeMillis;
        this.tryCount = tryCount;
    }

    public String getKey() {
   
        return key;
    }

    public void setKey(String key) {
   
        this.key = key;
    }

    public UUID getUuid() {
   
        return uuid;
    }

    public long getLockTimeout() {
   
        return lockTimeout;
    }

    public void setLockTimeout(long lockTimeout) {
   
        this.lockTimeout = lockTimeout;
    }

    public long getGetLockTimeMillis() {
   
        return getLockTimeMillis;
    }

    public void setGetLockTimeMillis(long getLockTimeMillis) {
   
        this.getLockTimeMillis = getLockTimeMillis;
    }

    public long getStartLockTimeMillis() {
   
        return startLockTimeMillis;
    }

    public void setStartLockTimeMillis(long startLockTimeMillis) {
   
        this.startLockTimeMillis = startLockTimeMillis;
    }

    public int getTryCount() {
   
        return tryCount;
    }

    public void setTryCount(int tryCount) {
   
        this.tryCount = tryCount;
    }
}  
@Component
public class RedisLockUtil {
   

    private static final Logger logger = LoggerFactory.getLogger(RedisDemo.class)<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值