基于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();
                }

            }
        }
    }
}

你可能感兴趣的:(Java)