redis 实现 分布式锁 优化方法

  传统方式,通过setnx实现,不多说直接上代码;

```
//查询或设置锁
public String acquireLock(Jedis conn, String lockName) {
    return acquireLock(conn, lockName, 10000);
}
public String acquireLock(Jedis conn, String lockName, long acquireTimeout) {
    String identifier = UUID.randomUUID().toString();

    long end = System.currentTimeMillis() + acquireTimeout;
    while (System.currentTimeMillis() < end) {
        if (conn.setnx("lock:" + lockName, identifier) == 1) {
            return identifier;
        }
        try {
            Thread.sleep(1);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }
        return null;
}

“`

//释放锁
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
    String lockKey = "lock:" + lockName;

    while (true){
        conn.watch(lockKey);
        if (identifier.equals(conn.get(lockKey))){
            Transaction trans = conn.multi();
            trans.del(lockKey);
            List<Object> results = trans.exec();
            if (results == null){
                continue;
            }
            return true;
        }

        conn.unwatch();
        break;
    }

    return false;
}

//多线程触发锁进行测试
public class PollQueueThread
    extends Thread
{
    private Jedis conn;
    private boolean quit;
    private Gson gson = new Gson();

    public PollQueueThread(){
        this.conn = new Jedis("localhost");
        this.conn.select(15);
    }

    public void quit() {
        quit = true;
    }

    public void run() {
        while (!quit){
            Set<Tuple> items = conn.zrangeWithScores("delayed:", 0, 0);
            Tuple item = items.size() > 0 ? items.iterator().next() : null;
            if (item == null || item.getScore() > System.currentTimeMillis()) {
                try{
                    sleep(10);
                }catch(InterruptedException ie){
                    Thread.interrupted();
                }
                continue;
            }

            String json = item.getElement();
            String[] values = gson.fromJson(json, String[].class);
            String identifier = values[0];
            String queue = values[1];

            String locked = acquireLock(conn, identifier);
            if (locked == null){
                continue;
            }

            if (conn.zrem("delayed:", json) == 1){
                conn.rpush("queue:" + queue, json);
            }

            releaseLock(conn, identifier, locked);
        }
    }
}

这种方式的缺点很明显, 每次循环都要sleep(10),这样对服务器以及redis都带来了多余的压力;

  查了下资料,redis的java客户端工具redisson,它内部包含的lock、tryLock等方法可以优雅的解决这个问题;

  maven中引入redisson的jar包,目前暂时无法直接用XML配置的方式配置连接池或者主从redis等参数,所以只能手写一个分布式锁的连接客户端;


package com.caocao.util.redis;

import org.apache.commons.lang.StringUtils;
import org.redisson.Config;
import org.redisson.MasterSlaveServersConfig;
import org.redisson.Redisson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

@Component("disLockClientTemplate")
public class DisLockClientTemplate {

    private static final Logger log = LoggerFactory.getLogger(DisLockClientTemplate.class);

    @Value("${redis.host}")
    private String host;

    @Value("${redis.port}")
    private int port;

    @Value("${redis.slaves}")
    private String slaves;

    @Value("${redis.password}")
    private String password;

    private Redisson redisson;

    public DisLockClientTemplate() {
    }

    @PostConstruct
    public void initRedisson() {
        Config config = new Config();
        MasterSlaveServersConfig mssConf = config.useMasterSlaveConnection().setMasterAddress(host+":"+port);
        mssConf.setPassword(password);
        //集群部署redis时获取slaves,暂时保留
        /*if (StringUtils.isNotBlank(slaves)) {
            for (String slave : slaves.split(",")) {
                mssConf.addSlaveAddress(slave);
            }
        }*/
        redisson = Redisson.create(config);
        log.info("init Redisson DONE!");
    }

    public Redisson getRedisson() {
        return redisson;
    }
}

  调用锁

public class TestDisLock1 {
    public static void main(String[] args) {
        String filePath = "classpath:conf/applicationContext.xml";
        ClassPathXmlApplicationContext context =  new ClassPathXmlApplicationContext(filePath);
        context.start();
        DisLockClientTemplate disLockClientTemplate  = (DisLockClientTemplate) context.getBean("disLockClientTemplate");
        Redisson redisson = disLockClientTemplate.getRedisson();
        RLock lock = redisson.getLock("haogrgr");
        try {
            lock.tryLock(1000,2000, TimeUnit.MILLISECONDS);
            System.out.println("testLock2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
}

  这里有同学会问,那redisson如何实现Lock锁的呢?跟第一种比有什么好处?

  具体的实现类是RedissonLock, 看看实现原理. 先看看中例子执行时, 所运行的命令(通过monitor命令):

127.0.0.1:6379> monitor
OK
1434959509.494805 [0 127.0.0.1:57911] "SETNX" "haogrgr" "{\"@class\":\"org.redisson.RedissonLock$LockValue\",\"counter\":1,\"id\":\"c374addc-523f-4943-b6e0-c26f7ab061e3\",\"threadId\":1}"
1434959509.494805 [0 127.0.0.1:57911] "GET" "haogrgr"//锁定
1434959509.524805 [0 127.0.0.1:57911] "MULTI"
1434959509.529805 [0 127.0.0.1:57911] "DEL" "haogrgr"//释放锁
1434959509.529805 [0 127.0.0.1:57911] "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"//通知其他等待线程
1434959509.529805 [0 127.0.0.1:57911] "EXEC"

  可以看到, 大概原理是, 通过判断Redis中是否有某一key, 来判断是加锁还是等待, 最后的publish是一个解锁后, 通知阻塞在lock的线程.

  分布式锁的实现依赖的单点, 这里Redis就是单点, 通过在Redis中维护状态信息来实现全局的锁. 那么来看看RedissonLock如何
实现可重入, 保证原子性等等细节.


Lock源码

    public void lock() {
        try {
            lockInterruptibly();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }

    public void lockInterruptibly() throws InterruptedException {
        lockInterruptibly(-1, null);    //leaseTime : -1 表示key不设置过期时间
    }

    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        Long ttl;
        if (leaseTime != -1) {
            ttl = tryLockInner(leaseTime, unit);
        } else {
            ttl = tryLockInner();
        }
        // lock acquired
        if (ttl == null) {
            return;
    }

    subscribe().awaitUninterruptibly();

    try {
        while (true) {
            if (leaseTime != -1) {
                ttl = tryLockInner(leaseTime, unit);
            } else {
                ttl = tryLockInner();
            }
            // lock acquired
            if (ttl == null) {
                break;
            }

            // waiting for message
            RedissonLockEntry entry = ENTRIES.get(getEntryName());
            if (ttl >= 0) {
                entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                entry.getLatch().acquire();
            }
        }
    } finally {
        unsubscribe();
    }
}

tryLockInner源码

private Long tryLockInner() {
    //保存锁的状态: 客户端UUID+线程ID来唯一标识某一JVM实例的某一线程
    final LockValue currentLock = new LockValue(id, Thread.currentThread().getId());   
    //用来保存重入次数, 实现可重入功能, 初始情况是1 
    currentLock.incCounter();    

    //Redisson封装了交互的细节, 具体的逻辑为execute方法逻辑.
    return connectionManager.write(getName(), new SyncOperation<LockValue, Long>() {

        @Override
        public Long execute(RedisConnection<Object, LockValue> connection) {
            //如果key:haogrgr不存在, 就set并返回true, 否则返回false
            Boolean res = connection.setnx(getName(), currentLock);    
            //如果设置失败, 那么表示有锁竞争了, 于是获取当前锁的状态, 如果拥有者是当前线程, 就累加重入次数并set新值
            if (!res) {    
                //通过watch命令配合multi来实现简单的事务功能
                connection.watch(getName());    
                LockValue lock = (LockValue) connection.get(getName());
                 //LockValue的equals实现为比较客户id和threadid是否一样
                if (lock != null && lock.equals(currentLock)) {   
                    //如果当前线程已经获取过锁, 则累加加锁次数, 并set更新
                    lock.incCounter();    
                    connection.multi();
                    connection.set(getName(), lock);
                    if (connection.exec().size() == 1) {
                        return null;    //set成功, 
                    }
                }
                connection.unwatch();
                //走到这里, 说明上面set的时候, 其他客户端在  watch之后->set之前 有其他客户端修改了key值
                //则获取key的过期时间, 如果是永不过期, 则返回-1, 具体处理后面说明
                Long ttl = connection.pttl(getName());
                return ttl;
            }
            return null;
        }
    });
}

tryLockInner的逻辑已经看完了, 可以知道, 有三种情况:

(1) key不存在, 加锁:

  当key不存在时, 设置锁的初始状态并set, 具体来看就是 setnx haogrgr LockValue{ id: Redisson对象的id, threadId: 当前线程id, counter: 当前重入次数,这里为第一次获取,所以为1

  通过上面的操作. 达到获取锁的目的, 通过setnx来达到实现类似于 if(map.get(key) == null) { map.put(key) } 的功能, 防止多个客户端同时set时, 新值覆盖老值.

(2)key存在, 且获取锁的当前线程, 重入:

  这里就是锁重入的情况, 也就是锁的拥有者第二次调用lock方法, 这时, 通过先get, 然后比较客户端ID和当前线程ID来判断拥有锁的线程是不是当前线程.(客户端ID+线程ID才能唯一定位锁拥有者线程)

  判断发现当前是重入情况, 则累加LockValue的counter, 然后重新set回去, 这里使用到了watch和multi命令, 防止 get -> set 期间其他客户端修改了key的值.

(3)key存在, 且是其他线程获取的锁, 等待:

  首先尝试获取锁(setnx), 失败后发现锁拥有者不是当前线程, 则获取key的过期时间, 返回过期时间

那么接下来看看tryLockInner调用完成后的代码.


    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        Long ttl;
        if (leaseTime != -1) {
            ttl = tryLockInner(leaseTime, unit);
        } else {
            ttl = tryLockInner();   //lock()方法调用会走的逻辑
        }
        // lock acquired
        //加锁成功(新获取锁, 重入情况) tryLockInner会返回null, 失败会返回key超时时间, 或者-1
        (key未设置超时时间)
        if (ttl == null) {   
            //加锁成功, 返回
            return;   
    }

        //subscribe这个方法代码有点多, Redisson通过netty来和redis通讯, 然后subscribe返回的是一个Future类型,
        //Future的awaitUninterruptibly()调用会阻塞, 然后Redisson通过Redis的pubsub来监听unlock的topic(getChannelName())
        //例如, 5中所看到的命令 "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"
        //当解锁时, 会向名为 getChannelName() 的topic来发送解锁消息("0")
        //而这里 subscribe() 中监听这个topic, 在订阅成功时就会唤醒阻塞在awaitUninterruptibly()的方法. 
        //所以线程在这里只会阻塞很短的时间(订阅成功即唤醒, 并不代表已经解锁)
        subscribe().awaitUninterruptibly();

        try {
            while (true) {    //循环, 不断重试lock
                if (leaseTime != -1) {
                    ttl = tryLockInner(leaseTime, unit);
                } else {
                    ttl = tryLockInner();   //不多说了
                }
                // lock acquired
                if (ttl == null) {
                    break;
                }


                // 这里才是真正的等待解锁消息, 收到解锁消息, 就唤醒, 然后尝试获取锁, 成功返回, 失败则阻塞在acquire().
                // 收到订阅成功消息, 则唤醒阻塞上面的subscribe().awaitUninterruptibly();
                // 收到解锁消息, 则唤醒阻塞在下面的entry.getLatch().acquire();
                RedissonLockEntry entry = ENTRIES.get(getEntryName());
                if (ttl >= 0) {
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    entry.getLatch().acquire();
                }
            }
        } finally {
            unsubscribe();  //加锁成功或异常,解除订阅``
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值