面试连环问之Redis分布式锁
以抽奖为例,介绍Redis分布式锁,锁住奖品库存,是如何一步步升级到可用,回答面试官的Redis锁连环问。
·
面试连环问之Redis分布式锁
面试连环问之Redis分布式锁
核心
- Redis分布式锁的小总结:
- 1、只使用Redis计数,线程不安全,有超卖的问题,解决方案:加ReenterLock或者synchronized解决
- 2、加ReenterLock或者synchronized是虚拟机层面的,解决不了分布式部署,解决方案:Redis setnx命令
- 3、如果占用setnx的线程异常一直不释放锁,导致后续用户无法执行逻辑,解决方案:setnx命令超时时间,并且在finally中释放setnx锁
- 4、setnx 命令加锁和设置过期时间不是原子操作,导致执行不成功,解决方案:setnx命令和加锁时间设置为原子操作
- 5、释放setnx锁时,错误的删除了别人的锁,解决方案:释放锁之前判断是否是本线程持有
- 6、释放锁之前判断是否是本线程持有 的过程不是原子操作的,解决方案:使用lua脚本,保证原子性
- 7、确保Redis锁的超时时间大于逻辑执行时间,否则仍然执行不了操作,解决方案:加守护进程
- 8、终极解决方案:使用redisson分布式锁,注意解锁之前是否本线程所持有
主要介绍
- 以抽奖为例,Redis锁住奖品库存,是如何一步步升级到可用,回答面试官的Redis锁连环问。
主要代码
1、新建RedisLock
@Component
@Slf4j
public class RedisLock {
private final String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end";
@Autowired
private RedisTemplate redisTemplate;
public boolean tryLock(String lockKey, String lockUuid, Long expireTime, Long tryTime, TimeUnit timeUnit) {
long endTime = System.currentTimeMillis() + timeUnit.toMillis(tryTime);
while (System.currentTimeMillis() < endTime) {
try {
RedisCallback<Boolean> callback = (connection) ->
connection.set(lockKey.getBytes(StandardCharsets.UTF_8), lockUuid.getBytes(StandardCharsets.UTF_8),
Expiration.seconds(timeUnit.toSeconds(endTime)), RedisStringCommands.SetOption.SET_IF_ABSENT);
return (boolean) redisTemplate.execute(callback);
} catch (Exception e) {
log.error("异常信息", e);
}
}
return false;
}
public boolean unlock(String lockKey, String lockUuid) {
try {
RedisCallback<String> callback = (connection) -> connection.eval(script.getBytes(StandardCharsets.UTF_8),
ReturnType.BOOLEAN, 1, lockKey.getBytes(StandardCharsets.UTF_8), lockUuid.getBytes(StandardCharsets.UTF_8));
return (boolean) redisTemplate.execute(callback);
} catch (Exception e) {
log.error("异常信息", e);
}
return false;
}
}
2、使用Redis锁遇到的问题
@Component
@Slf4j
public class DemoRedisLock {
@Autowired
private RedisUtils redisUtils;
@Autowired
private Redisson redisson;
@Autowired
private RedisLock redisLock;
public final String LUCK_REDIS_LOCK = "LUCK:REDIS:LOCK:";
public final String LUCK_REDIS_NX_LOCK = "LUCK:REDIS:NX:LOCK:";
/**
* 终极解决方案1,redisson
*
* @param luckId
* @return
*/
public String getAwardF1(String luckId) throws InterruptedException {
RLock rLock = redisson.getLock(LUCK_REDIS_NX_LOCK);
try {
if (rLock.tryLock(3L, 6L, TimeUnit.SECONDS)) {
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
}
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
} finally {
if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
throw new RuntimeException("抽奖已经结束");
}
/**
* 终极解决方案2
*
* @param luckId
* @return
* @throws InterruptedException
*/
public String getAwardF2(String luckId) {
String uuid = UUID.randomUUID().toString();
;
try {
if (redisLock.tryLock(LUCK_REDIS_LOCK, uuid, 3L, 5L, TimeUnit.SECONDS)) {
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
}
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
} finally {
redisLock.unlock(LUCK_REDIS_LOCK, uuid);
}
throw new RuntimeException("抽奖已经结束");
}
/**
* 抽奖1
* 带来的问题:
* 1、线程不安全,有超卖的问题,
* 如何解决:
* 2、加ReenterLock或者synchronized解决?
*
* @return
*/
public String getAward1(String luckId) {
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
}
/**
* 抽奖2
* 带来的问题:
* 1、加ReenterLock或者synchronized是虚拟机层面的,解决不了分布式部署
* 如何解决:
* 2、使用Redis setnx(如果key存在则设置不成功,如果key不存在,才可以设置成功)解决?
* https://redis.io/commands/set/
*
* @return
*/
public String getAward2(String luckId) {
synchronized (this) {
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
}
}
/**
* 抽奖3
* 带来的问题:
* 1、如果占用setnx的线程异常一直不释放锁,导致后续用户无法抽奖,
* 如何解决:
* 2、新增setnx命令超时时间,并且在finally中释放setnx锁
*
* @return
*/
public String getAward3(String luckId) {
String uuid = UUID.randomUUID().toString();
boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
if (!success) {
throw new RuntimeException("抽奖已经结束");
}
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
}
/**
* 抽奖4
* 带来的问题:
* 1、setnx 命令加锁和设置过期时间不是原子操作,导致执行不成功
* 如何解决:
* 2、setnx命令和加锁时间设置为原子操作
*
* @return
*/
public String getAward4(String luckId) {
String uuid = UUID.randomUUID().toString();
try {
boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid);
this.redisUtils.expire(LUCK_REDIS_NX_LOCK + luckId, 3L);
if (!success) {
throw new RuntimeException("抽奖已经结束");
}
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
} finally {
redisUtils.delKey(LUCK_REDIS_NX_LOCK);
}
}
/**
* 抽奖5
* 带来的问题:
* 1、释放setnx锁时,错误的删除了别人的锁
* 如何解决:
* 2、释放锁之前判断是否是本线程持有
*
* @return
*/
public String getAward5(String luckId) {
String uuid = UUID.randomUUID().toString();
try {
boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
if (!success) {
throw new RuntimeException("抽奖已经结束");
}
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
} finally {
//删除其他线程的锁
redisUtils.delKey(LUCK_REDIS_NX_LOCK);
}
}
/**
* 抽奖6
* 带来的问题:
* 1、释放锁之前判断是否是本线程持有 的过程不是原子操作的
* 如何解决:
* 2、使用Lua脚本,保证原子性
*
* @return
*/
public String getAward6(String luckId) {
String uuid = UUID.randomUUID().toString();
try {
boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
if (!success) {
throw new RuntimeException("抽奖已经结束");
}
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
} finally {
Object value = this.redisUtils.getString(LUCK_REDIS_NX_LOCK);
if (StringUtils.equals(uuid, value.toString())) {
redisUtils.delKey(LUCK_REDIS_NX_LOCK);
}
}
}
/**
* 抽奖7
* 带来的问题:
* 1、确保Redis锁的超时时间大于抽奖执行时间(执行减库存逻辑超过过期时间,这个时候还没有减库存,但是setnx锁已经释放,其他线程已经进入,仍然可以减库存)
* 如何解决:
* 2、守护线程
*
* @return
*/
public String getAward7(String luckId) {
String uuid = UUID.randomUUID().toString();
try {
boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
if (!success) {
throw new RuntimeException("抽奖已经结束");
}
int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
if (number > 0) {
int leftNumber = number - 1;
//.... 这里会出现执行其他逻辑(假如是调用第三方系统,时间超过了Redis setnx的过期时间,这个时候还没有减库存,但是setnx锁已经释放,其他线程已经进入)
this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
log.info("抽奖奖品还剩余:{}", leftNumber);
return "抽奖成功";
} else {
throw new RuntimeException("抽奖太火爆了,请稍后再试");
}
} finally {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end";
Jedis jedis = this.redisUtils.getJedis();
String value = (String) redisUtils.getString(LUCK_REDIS_NX_LOCK);
Object eval = jedis.eval(script, Collections.singletonList(LUCK_REDIS_NX_LOCK), Collections.singletonList(value));
if (StringUtils.equals(eval.toString(), "1")) {
redisUtils.delKey(LUCK_REDIS_NX_LOCK);
}
}
}
}
更多推荐
已为社区贡献1条内容
所有评论(0)