• redis 读写锁实现


    先搞清楚读写锁要做什么。

    基本就是
    读读不互斥,读写互斥,写写互斥。可重入。

    关于redis读写锁,我写了一次之后,总觉得很怪,然后就上网看到大神的redisson了,果断借鉴一番。

    读行为

    当写锁未获取,加上读锁(通知其他请求数据在读状态),读数据

    当写锁被获取,等待,直到写锁未获取,加读锁,读数据

    写行为

    当写锁未获取,等待获取写锁

    当写锁被获取,加写锁。读锁未获取,等待获取读锁

    当写锁被获取,读锁被获取,写数据

    可以看出读锁可重入一定意义都没有,写锁才有意义

    三 初版

    先说下总结

    1.重入也只是本机重入,不能实现锁在其他服务器的重入。

    2.读写锁获取锁的时候,是两个redis操作,原子性不行,所以要用redis的eval命令或者直接使用lua脚本。

    3.用switch来判断读写模式太蠢了,代码可读性低,早期想的简单,但是逻辑一复杂就很麻烦了。

    ps.

    spring自带的redisTemplate则没有提供eval的接口,只提供使用lua脚本,相应的读写锁代码要自己写。

    netty自带的redisson则是用了eval命令,则已经写好了代码,只需要傻瓜式调用就好了。

    代码

    --存放读写锁的信息
    public
    enum LockModel { READ("%s:READ"), WRITE("%s:WRITE"),; String lockFormat; LockModel(String lockFormat) { this.lockFormat = lockFormat; } public String getLockModelName() { return super.name(); } public String getLockFormat() { return lockFormat; } public static void main(String[] args) { LockModel read = LockModel.READ; System.out.println(read.getLockFormat()); System.out.println(read.getLockModelName()); } }
    --实现java自带的读写锁接口
    public class ReadWriteLock implements java.util.concurrent.locks.ReadWriteLock {
      /**
        * 应该是唯一标识组成的key,可以使线程id,可以使用户id,可以使服务器id
       */

    String name;
    /**
    * 毫秒
    * */
    Long timeInterval;

    public ReadWriteLock(String name, Long timeInterval) {
    this.name = name;
    this.timeInterval = timeInterval;
    }

    @Override
    public Lock readLock() {
    return new ReentrantLock(this, LockModel.READ);
    }

    @Override
    public Lock writeLock() {
    return new ReentrantLock(this, LockModel.WRITE);
    }

    }

    --重入锁
    public class ReentrantLock implements Lock {

    @Autowired
    RedisTemplate redisTemplate;

    ReadWriteLock rwLock;
    LockModel lockModel;
    String lockName;
    Long deadTime = 0L;
    boolean localWriteLocked = false;

    public ReentrantLock(ReadWriteLock rwLock, LockModel lockModel) {
    this.rwLock = rwLock;
    this.lockModel = lockModel;
    setLockName(lockModel);
    }

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

    @Override
    public void lockInterruptibly() throws InterruptedException {
    switch (getLockModel()) {
    case WRITE:
    if (!isLocalWriteLocked()) {
    setLockModel(LockModel.READ);
    while (!tryLock()) {
    Thread.sleep(500);
    }
    redisTemplate.opsForValue().set(getLockName(), getDeadTime(), getRwLock().getTimeInterval());

    setLockModel(LockModel.WRITE);
    while (!tryLock()) {
    Thread.sleep(500);
    }
    setLocalWriteLocked(true);
    } else {
    /**
    * 本机持有写锁,重入,但要等待之前的写操作完成
    * */
    while (!isLocalWriteLocked()) {
    Thread.sleep(500);
    }

    /**
    * 更新写锁的过期时间
    * */
    redisTemplate.opsForValue().set(getLockName(), getDeadTime(), getRwLock().getTimeInterval());
    setLocalWriteLocked(true);
    }
    break;
    case READ:
    while (!tryLock()) {
    Thread.sleep(500);
    }
    setDeadTime();
    redisTemplate.opsForValue().set(getLockName(), getDeadTime(), getRwLock().getTimeInterval());
    break;
    }
    }

    @Override
    public boolean tryLock() {
    return null != redisTemplate.opsForValue().get(getOpposeLockName());
    }

    @Override
    public void unlock() {
    switch (getLockModel()) {
    case WRITE:
    if (isLocalWriteLocked()) {
    setLocalWriteLocked(false);
    }
    redisTemplate.delete(getLockName());
    break;
    case READ:
    redisTemplate.delete(getLockName());
    break;
    }
    }


    public Long getTimeInterval() {
    return rwLock.getTimeInterval();
    }

    public void setDeadTime() {
    this.deadTime = System.currentTimeMillis() + getTimeInterval();
    }

    private String getOpposeLockName() {
    String opposeLockName = "";
    switch (getLockModel()) {
    case READ:
    opposeLockName = String.format(LockModel.WRITE.getLockFormat(), getRwLock().getName());
    break;
    case WRITE:
    opposeLockName = String.format(LockModel.READ.getLockFormat(), getRwLock().getName());
    break;
    default:
    break;
    }
    return opposeLockName;
    }

    }

     

    四 redisson分析

    还是先总结

    1.用hashmap存读写锁的信息。读锁写锁的本质则是model的不同。读锁写锁只是不同的mapfield。而读锁还有过期时间为属性。

    2.用频道记录线程的操作。具体为什么用频道就要看LockPubSub和PublishSubscribe,这里因为不涉及我就不细说了。

    RedissonReadLock

    //    判断有没有锁
      @Override
    public boolean isLocked() { RFuture<String> future = commandExecutor.writeAsync(getName(), StringCodec.INSTANCE, RedisCommands.HGET, getName(), "mode"); String res = get(future); return "read".equals(res); }

    可以看出尝试获取锁的状态的代码都写的很简单,但是redisson用了hashmap来存放。

        @Override
        <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
            internalLockLeaseTime = unit.toMillis(leaseTime);
    
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                                    "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                         //锁出错 "if (mode == false) then " + "redis.call('hset', KEYS[1], 'mode', 'read'); " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('set', KEYS[2] .. ':1', 1); " + "redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
                         //在读模式或者本线程获取写锁的时候进行读 "if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " + "local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "local key = KEYS[2] .. ':' .. ind;" + "redis.call('set', key, 1); " + "redis.call('pexpire', key, ARGV[1]); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end;" + "return redis.call('pttl', KEYS[1]);", Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)), internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId)); }

    注意里面的getWriteLockName(threadId)

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
            String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
            String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
    
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                    "if (mode == false) then " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                    "end; " +
              //锁不存在 "local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " + "if (lockExists == 0) then " + "return nil;" + "end; " + //给读锁的值-1,返回结果值 "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
              // 结果值为0,删除读锁 "if (counter == 0) then " + "redis.call('hdel', KEYS[1], ARGV[2]); " + "end;" +
              //  把自己的超时标记删除 "redis.call('del', KEYS[3] .. ':' .. (counter+1)); " + //还有其他读 "if (redis.call('hlen', KEYS[1]) > 1) then " + "local maxRemainTime = -3; " + "local keys = redis.call('hkeys', KEYS[1]); " + "for n, key in ipairs(keys) do " + "counter = tonumber(redis.call('hget', KEYS[1], key)); " + "if type(counter) == 'number' then " + "for i=counter, 1, -1 do " + "local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " + "maxRemainTime = math.max(remainTime, maxRemainTime);" + "end; " + "end; " + "end; " + "if maxRemainTime > 0 then " + "redis.call('pexpire', KEYS[1], maxRemainTime); " + "return 0; " + "end;" + //有写锁直接返回 "if mode == 'write' then " + "return 0;" + "end; " + "end; " + "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; ", Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix), LockPubSub.unlockMessage, getLockName(threadId)); }

    解锁还给其他锁续命,,,最大存活时间maxRemainTime很有意思,存在就给他加上等量的剩余存活时间,而不是固定加多少。那是不是无限续然后过期不了?但是这里是读写锁的存活时间而不是读锁的时间。

    并且publish到相应的频道,更新状态。

        protected RFuture<Boolean> renewExpirationAsync(long threadId) {
            String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
            String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
            
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "local counter = redis.call('hget', KEYS[1], ARGV[2]); " +
                //不是false "if (counter ~= false) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "if (redis.call('hlen', KEYS[1]) > 1) then " + "local keys = redis.call('hkeys', KEYS[1]); " + "for n, key in ipairs(keys) do " + "counter = tonumber(redis.call('hget', KEYS[1], key)); " + "if type(counter) == 'number' then " + "for i=counter, 1, -1 do " + "redis.call('pexpire', KEYS[2] .. ':' .. key .. ':rwlock_timeout:' .. i, ARGV[1]); " + "end; " + "end; " + "end; " + "end; " + "return 1; " + "end; " + "return 0;", Arrays.<Object>asList(getName(), keyPrefix), internalLockLeaseTime, getLockName(threadId)); }

    刷新存活时间没啥特殊的

        @Override
        public RFuture<Boolean> forceUnlockAsync() {
            cancelExpirationRenewal(null);
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "if (redis.call('hget', KEYS[1], 'mode') == 'read') then " +
                        "redis.call('del', KEYS[1]); " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                    "end; " +
                    "return 0; ",
                    Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage);
        }

    没有之前续命的操作了。并且整个删除

    RedissonWriteLock

        @Override
        public RFuture<Boolean> forceUnlockAsync() {
            cancelExpirationRenewal(null);
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                  "if (redis.call('hget', KEYS[1], 'mode') == 'write') then " +
                      "redis.call('del', KEYS[1]); " +
                      "redis.call('publish', KEYS[2], ARGV[1]); " +
                      "return 1; " +
                  "end; " +
                  "return 0; ",
                  Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.readUnlockMessage);
        }
    
        @Override
        public boolean isLocked() {
            RFuture<String> future = commandExecutor.writeAsync(getName(), StringCodec.INSTANCE, RedisCommands.HGET, getName(), "mode");
            String res = get(future);
            return "write".equals(res);
        }

    这两方法和读锁类似就不说了,而且增加过期时间写锁不支持这功能

     @Override
        <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
            internalLockLeaseTime = unit.toMillis(leaseTime);
    
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                                "if (mode == false) then " +
                                      "redis.call('hset', KEYS[1], 'mode', 'write'); " +
                                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                      "return nil; " +
                                  "end; " +
                                  "if (mode == 'write') then " +
                                      "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                          "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
                                          "local currentExpire = redis.call('pttl', KEYS[1]); " +
                                          "redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
                                          "return nil; " +
                                      "end; " +
                                    "end;" +
                                    "return redis.call('pttl', KEYS[1]);",
                            Arrays.<Object>asList(getName()), 
                            internalLockLeaseTime, getLockName(threadId));
        }

    显然,如果写锁是这个线程持有的才可以进行写操作。

        @Override
        protected RFuture<Boolean> unlockInnerAsync(long threadId) {
            String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
            String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
    
            return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                    "if (mode == false) then " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                    "end; " +
                    "local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
                    "if (lockExists == 0) then " +
                        "return nil;" +
                    "end; " +
                        
                    "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " + 
                    "if (counter == 0) then " +
                        "redis.call('hdel', KEYS[1], ARGV[2]); " + 
                    "end;" +
                    "redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
                    
                    "if (redis.call('hlen', KEYS[1]) > 1) then " +
                        "local maxRemainTime = -3; " + 
                        "local keys = redis.call('hkeys', KEYS[1]); " + 
                        "for n, key in ipairs(keys) do " + 
                            "counter = tonumber(redis.call('hget', KEYS[1], key)); " + 
                            "if type(counter) == 'number' then " + 
                                "for i=counter, 1, -1 do " + 
                                    "local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " + 
                                    "maxRemainTime = math.max(remainTime, maxRemainTime);" + 
                                "end; " + 
                            "end; " + 
                        "end; " +
                                
                        "if maxRemainTime > 0 then " +
                            "redis.call('pexpire', KEYS[1], maxRemainTime); " +
                            "return 0; " +
                        "end;" + 
                            
                        "if mode == 'write' then " + 
                            "return 0;" + 
                        "end; " +
                    "end; " +
                        
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; ",
                    Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix), 
                    LockPubSub.unlockMessage, getLockName(threadId));
        }

    这里的也是给读写锁续命,看来就是数据使用次数越多读写锁存活的时间越长,而具体的读锁写锁的存活时间则是hashmap里面的一个属性。

  • 相关阅读:
    渲染机制
    CSS渲染速度改善的十个方法与建议
    HTML你应该知道的三大基本元素
    链接属性rel=’external’、rel=’nofollow’、rel=’external nofollow’三种写法的区别
    rel=nofollow
    CSS3 圆形时钟式网页进度条
    @media screen解决移动web开发的多分辨率问题
    meta 标签属性(网站兼容与优化需要)
    如何解决谷歌浏览器下jquery无法获取图片的尺寸
    CSS设计之页面滚动条出现时防止页面跳动的方法
  • 原文地址:https://www.cnblogs.com/ydymz/p/10120782.html
Copyright © 2020-2023  润新知