如何在项目中快速实现一个分布式读写锁

172 阅读3分钟

一、前言

最近在工作中遇到一个场景,需要处理两种用户群体因竞争订单资源产生的并发安全问题。

A群体只有一个人,B群体有很多人。当A群体抢到锁的时候,B群体所有人都抢不到;当B群体的某个人抢到锁的时候,B群体的其他人也可以抢到,但是A群体抢不到。 image.png

这场景不就恰巧可以使用读写锁吗?让A群体抢写锁,B群体抢读锁。读读支持,读写互斥。于是便想着能不能自己实现这个功能。


二、锁的数据结构

锁可以通过redis的string数据类型来实现,读锁和写锁共用一个key,通过value来区分读与写,读锁的value是"read",而写锁的value是"write"。

key的组成可以包含一些变量,比如订单号。


三、核心:lua脚本逻辑

上锁的过程包含查询和赋值两个操作,为了保证多个操作的原子性,使用lua脚本

上读锁的时候,先查询锁,如果锁不存在或者已经有读锁了,则直接上读锁,并设置超时时间,并返回成功;其他情况直接返回失败。

上写锁的时候,先查询锁,如果锁不存在,则直接上写锁,并设置超时时间,并返回成功;其他情况直接返回失败。

另外,读锁也是可重入的。

    "local mode = redis.call('GET' , KEYS[1]) " +
    "if (mode == false) or (ARGV[1] == 'read' and mode == 'read') then " +
        "redis.call('SET' , KEYS[1] , ARGV[1]) " +
        "redis.call('PEXPIRE' , KEYS[1] , ARGV[2]) " +
        "return '1' " +
    "end; " +
    "return '0' ";

四、锁的超时时间

由使用方去设置超时时间,根据业务情况区评估一个合理的值。

需要注意的是,成功上锁之后,业务代码的执行时间要尽量确保不会超过锁的超时时间,否则可能导致业务还没执行完,锁却超时释放了。个别异常情况下,可能会出现这种问题。


五、锁的释放

直接把key删除。


六、锁续约和锁误删问题

极端情况下确实可能会出现这两种问题。对于本次的业务场景来说,正常情况业务代码耗时较短,只要设置合理的超时时间,基本不会有问题。

如果是一些高并发并且无法容忍这两种问题的业务场景,建议使用Redisson框架。


七、在redis cluster的环境下使用jedis执行lua脚本

private Object jedisExecuteLuaScript(String script, List<String> keys, List<String> args) {
        return redisTemplate.execute((RedisCallback<Object>) connection -> {
            // 获取连接
            Object nativeConnection = connection.getNativeConnection();
            // 集群模式的实例
            if (nativeConnection instanceof JedisCluster) {
                return ((JedisCluster) nativeConnection).eval(script, keys, args);
            }
            // 单机模式的实例
            if (nativeConnection instanceof Jedis) {
                return ((Jedis) nativeConnection).eval(script, keys, args);
            }
            return null;
        });
    }

八、封装成工具类,供其他场景复用

完整代码如下

@Component
public class RedisReadWriteLock {
    private static final String lockScript =
    "local mode = redis.call('GET' , KEYS[1]) " +
    "if (mode == false) or (ARGV[1] == 'read' and mode == 'read') then " +
        "redis.call('SET' , KEYS[1] , ARGV[1]) " +
        "redis.call('PEXPIRE' , KEYS[1] , ARGV[2]) " +
        "return '1' " +
    "end; " +
    "return '0' ";

    private static final String delScript = "redis.call('DEL', KEYS[1])  return '1' ";

    private static final String LOCK_PREFIX = "r_bid_rwlock:";
    private static final String LOCK_SUCCESS = "1";
    private static final String READ_MODE = "read";
    private static final String WRITE_MODE = "write";

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 获取读锁
     */
    public boolean getReadLock(String key, Long expiredMillSecond, Long waitMillSecond, boolean printLog) {
        return lock(true, key, expiredMillSecond, waitMillSecond, printLog);
    }

    /**
     * 获取写锁
     */
    public boolean getWriteLock(String key, Long expiredMillSecond, Long waitMillSecond, boolean printLog) {
        return lock(false, key, expiredMillSecond, waitMillSecond, printLog);
    }

    /**
     * 释放锁
     **/
    public void releaseLock(String key) {
        jedisExecuteLuaScript(delScript, Collections.singletonList(getFinalKey(key)), Collections.EMPTY_LIST);
    }

    private String getFinalKey(String key) {
        return LOCK_PREFIX + key;
    }

    private boolean lock(boolean isRead, String key, long expiredMillSecond, long waitMillSecond, boolean printLog) {
        String finalKey = getFinalKey(key);
        String mode = isRead ? READ_MODE : WRITE_MODE;
        long begin = System.currentTimeMillis();

        while (!LOCK_SUCCESS.equals(jedisExecuteLuaScript(lockScript, Collections.singletonList(finalKey),
                                                          Arrays.asList(mode, Long.toString(expiredMillSecond))))) {
            if (begin + waitMillSecond < System.currentTimeMillis()) {
                if (printLog) {
                    logger.error("锁超时:" + finalKey + "-" + mode);
                }
                return false;
            }
            try {
                if (printLog) {
                    logger.info("等待锁:" + finalKey + "-" + mode);
                }
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        if (printLog) {
            logger.info("拿到锁:" + finalKey + "-" + mode);
        }
        return true;
    }

    /**
     * jedis 执行lua脚本
     */
    private Object jedisExecuteLuaScript(String script, List<String> keys, List<String> args) {
        return redisTemplate.execute((RedisCallback<Object>) connection -> {
            // 获取连接
            Object nativeConnection = connection.getNativeConnection();
            // 集群模式的实例
            if (nativeConnection instanceof JedisCluster) {
                return ((JedisCluster) nativeConnection).eval(script, keys, args);
            }
            // 单机模式的实例
            if (nativeConnection instanceof Jedis) {
                return ((Jedis) nativeConnection).eval(script, keys, args);
            }
            return null;
        });
    }
}