基於redis的分佈式鎖

摘要

當在分佈式模型下,數據只有一份,此時需要利用鎖的技術控制某一時刻修改數據的進程數。與單機模式下的鎖不同,分佈式鎖不僅僅需要保證不同進程訪問對象有鎖,還需要保證不同主機的不同系統訪問該對象時有鎖。所以通常我們會爲需要枷鎖的對象添加狀態,分佈式系統中鎖的狀態通常存儲在外部公共存儲中,例如redis、zookeeper、文件系統甚至數據庫中。
本文將基於redis實現分佈式鎖。

加鎖

加鎖其實就是向redis添加key-value。爲了避免死鎖,需要設置過期時間。

  • NX 代表只在鍵不存在時,纔對鍵進行設置操作
SET lock_key lock_value NX PX 5000

如果上面的命令執行成功,則證明客戶端獲取到了鎖。

解鎖

解鎖其實就是刪除redis中添加的key。但也不能亂刪,不能客戶端1的請求將客戶端2的鎖給刪除掉。需要根據lock_value過濾。需要注意的是爲了保證redis操作的原子性,需要通過lua命令刪除。

static {
    StringBuilder sb = new StringBuilder();
    sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then " );
    sb.append(" return redis.call('expire',KEYS[1],ARGV[2]) ");
    sb.append(" else ");
    sb.append("return 0 ");
    sb.append(" end ");
    lua_expire=sb.toString();
}
jedis.eval(lua_del, 1, lockKey, lockValue);

過期時間

爲了避免死鎖設置了過期時間,同時又要保證過期時間不能低於代碼執行時間。所以單獨添加一個線程刷新過期時間。同時爲了保證redis操作原子性,通過lua腳本執行。

/**
 * 開啓定時刷新
 */
protected void scheduleExpirationRenewal(){
    Thread renewalThread = new Thread(new ExpirationRenewal());
    renewalThread.start();
}

private class ExpirationRenewal implements Runnable{
    @Override
    public void run() {
        while (isOpenExpirationRenewal){
            try{
                System.out.println("[key="+lockKey+"]延遲失效時間");
                jedis.eval(lua_expire,1, lockKey, lockValue, String.valueOf(EXPIRE_TIME));
                //休眠10秒
                sleepBySecond(EXPIRE_TIME-1);
            }catch (Exception ex){
                ex.printStackTrace();
            }

        }
    }
}

使用方式

RedisLock lock = new RedisLock("testRedisLock");
lock.lock();
//模擬業務執行15秒
System.out.println("執行方法:"+id);
lock.sleepBySecond(15);
lock.unlock();

代碼

public class RedisLock implements Lock {
    private static final long EXPIRE_TIME=100;
    private static final String NOT_EXIST="NX";
    private static final String SECOND="EX";
    private static final String OK="OK";

    protected volatile boolean isOpenExpirationRenewal = true;

    private Jedis jedis=null;
    private String lockKey="";
    private String lockValue="";
    private static String lua_del= "";
    private static String lua_expire= "";

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then ");
        sb.append(" return redis.call('del', KEYS[1]) ");
        sb.append(" else ");
        sb.append("return 0 ");
        sb.append(" end ");
        lua_del=sb.toString();
    }

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then " );
        sb.append(" return redis.call('expire',KEYS[1],ARGV[2]) ");
        sb.append(" else ");
        sb.append("return 0 ");
        sb.append(" end ");
        lua_expire=sb.toString();
    }

    /**
     * 獲取redis連接,隨機生成value
     * @param lockKey
     */
    public RedisLock(String lockKey){
        this.lockKey=lockKey;
        this.lockValue= UUID.randomUUID().toString()+Thread.currentThread().getId();
        JedisPool pool=SpringContextUtil.getBean(JedisPool.class);
        this.jedis= pool.getResource();
    }

    /**
     * 加鎖
     */
    @Override
    public void lock() {
        while(true){
            String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECOND,EXPIRE_TIME);
            if(OK.equals(result)){
                System.out.println("[key="+lockKey+"]已加鎖");
                //添加單獨線程刷新過期時間
                isOpenExpirationRenewal = true;
                scheduleExpirationRenewal();
                break;
            }
        }
    }

    /**
     * 解鎖
     */
    @Override
    public void unlock() {
        jedis.eval(lua_del, 1, lockKey, lockValue);
        System.out.println("[key="+lockKey+"]已解鎖");
        isOpenExpirationRenewal = false;
    }

    @Override
    public void lockInterruptibly(){}

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit){
        return false;
    }

    /**
     * 線程休眠
     * @param second
     */
    public void sleepBySecond(long second){
        try {
            Thread.sleep(second*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    /**
     * 開啓定時刷新
     */
    protected void scheduleExpirationRenewal(){
        Thread renewalThread = new Thread(new ExpirationRenewal());
        renewalThread.start();
    }

    private class ExpirationRenewal implements Runnable{
        @Override
        public void run() {
            while (isOpenExpirationRenewal){
                try{
                    System.out.println("[key="+lockKey+"]延遲失效時間");
                    jedis.eval(lua_expire,1, lockKey, lockValue, String.valueOf(EXPIRE_TIME));
                    //休眠10秒
                    sleepBySecond(EXPIRE_TIME-1);
                }catch (Exception ex){
                    ex.printStackTrace();
                }

            }
        }
    }
}
發佈了36 篇原創文章 · 獲贊 28 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章