一、前言
最近在工作中遇到一个场景,需要处理两种用户群体因竞争订单资源产生的并发安全问题。
A群体只有一个人,B群体有很多人。当A群体抢到锁的时候,B群体所有人都抢不到;当B群体的某个人抢到锁的时候,B群体的其他人也可以抢到,但是A群体抢不到。
这场景不就恰巧可以使用读写锁吗?让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;
});
}
}