分布式应用下的Redis单机锁设计与实现

背景

最近写了一个定时任务,期望是同一时间只有一台机器运行即可。因为是应用是在集群环境下跑的,所以需要自己实现类一个简陋的Redis单机锁。

原理

主要是使用了Redis的SET NX特性,成功设置的那个客户端则被认为拿到了锁,没有设置成功的其他客户单则认为没有拿到锁。
在分布式环境下使用锁是挺危险的一件事情,我们可能会遇到一些问题:

  1. Redis单点故障;
  2. 应用与Redis网络不通;
  3. 应用异常导致锁没有得到释放;
  4. 误操作锁。

对于问题1,避免Redis单点故障,可以使用Redis分布式锁的实现,提供了多种语言的开源实现(http://redis.io/topics/distlock),也可以使用其他配置管理类组件实现(比如:zk、consul、etcd等),不在此文讨论。
对于问题2,我们需要把业务限定在不强依赖Redis锁的范围,虽然绝大多数情况不会发生问题,但是不能完全保证Redis锁不出问题。
对于问题3,为了防止异常引起的死锁情况,需要为每个锁设置超时时间,以确保不会因为应用问题导致无法释放锁。同时要设置一个合理的超时时间以免,达不到或者削弱锁的效果。
对于问题4,为每个锁存储一个标记,当解锁的时候,进行验证,用以保证每个客户端只能操作自己的锁。

实现

public class RedisLock {
    private static final Logger logger = LoggerFactory.getLogger(RedisLocker.class);
    // update on 2019-11-08 22:55:00 修改成线程安全版本
    private final Map<String, String> LOCK_MAP = new ConcurrentHashMap<String, String>(4); 
    private JedisPool pool;
    public enum EXPX {
        EX,PX
    }
    public boolean tryAcquire(String topic, EXPX expx,long time) throws Exception{
        if (!LOCK_MAP.containsKey(topic)) {
            String uuid = UUID.randomUUID().toString();
            Jedis jedis = null;
            // 存在key,返回空,不存在返回OK
            try {
                jedis = pool.getResource();
                String result = jedis.set(topic, uuid, "NX", expx.name(), time);
                logger.error("获取返回值,result {}", result);
                if ("OK".equals(result)) {
                    LOCK_MAP.put(topic, uuid);
                    return true;
                }
                logger.error("获取锁失败,result {}", result);
            } catch (Exception e) {
                pool.returnBrokenResource(jedis);
                throw e;
            }finally {
                pool.returnResource(jedis);
            }
        }
        return false;
    }
    public boolean unlock(String topic) {
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            String random = jedis.get(topic);
            if (random != null && random.equals(LOCK_MAP.get(topic))) {
                jedis.del(topic);
            }
            LOCK_MAP.remove(topic);
        } catch (Exception e) {
            pool.returnBrokenResource(jedis);
            throw e;
        } finally {
            pool.returnResource(jedis);
        }
        return true;
    }
    public void setPool(JedisPool pool) {
        this.pool = pool;
    }
}

总结

本文做了一种简单暴力的锁的实现。没有做高可用的方案,主要胜在轻量,简单,在平时场景可以使用。
除了使用配置中心相关的组件也是可以实现锁的,并且是分布式的锁。

关于RedisPool的两点需要注意:

  1. 在锁的场景,testOnBorrow应该为true;
  2. 设置合理的空闲连接数;
  3. 设置连接上限,防止占用过多资源。
posted @ 2016-04-18 17:25  土豆条  阅读(562)  评论(0编辑  收藏  举报