⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 c1n.cn/OZvGN 「十年培训经验的菜包」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

背景

企微报警群里连续发出生产环境报错警告,报错核心信息如下:

redis setNX error java.lang.NumberFormatException: For input string: "null"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
......

经异常信息定位,发现是项目中自定义的 Redis 分布式锁报错,并且该异常是在最近需求上线后突然出现,并且伴随该异常出现的,还有需求涉及的业务数据出现部分错乱的问题。

问题分析

老规矩,先贴涉及代码:

//切面
public class RedisLockAspect{
public void around(ProceedingJoinPoint pjp) {
String key = "...";
try {
//阻塞,直到获取锁为止
while (!JedisUtil.lock(key, timeOut)) {
Thread.sleep(10);
}
//执行业务逻辑
pjp.proceed();
}finally {
JedisUtil.unLock(key);
}
}
}

以上为自定义 Redis 分布式锁的切面,不看细节,只看整体逻辑,问题不大。

那再看实际加锁方法:

public class JedisUtil{
public static boolean lock(String key, long timeOut){
long currentTimeMillis = System.currentTimeMillis();
long newExpireTime = currentTimeMillis + timeOut;
RedisConnection connection = null;
try {
connection = getRedisTemplate().getConnectionFactory().getConnection();
Boolean setNxResult = connection.setNX(key.getBytes(StandardCharsets.UTF_8), String.valueOf(newExpireTime).getBytes(StandardCharsets.UTF_8));
//位置1
if(setNxResult){
expire(key,timeOut, TimeUnit.MILLISECONDS);
return true;
}
//位置2
Object objVal = getRedisTemplate().opsForValue().get(key);
String currentValue = String.valueOf(objVal);
//位置3,异常位置为if判断中Long.parseLong(currentValue),currentValue为null的字符串
if (currentValue != null && Long.parseLong(currentValue) < currentTimeMillis) {
String oldExpireTime = (String) getAndSet(key, String.valueOf(newExpireTime));
if (oldExpireTime != null && oldExpireTime.equals(currentValue)) {
return true;
}
}
}
return false;
}

public static void unLock(String key){
getRedisTemplate().delete(key);
}
}

有经验的大佬看到这段代码,估计会忍不住爆粗,但咱先不管,先看错误位置。

异常信息可以看出,currentValue 的值为字符串“null”,即 String.valueOf(objVal) 中的 objVal 对象为 null,也就是在 Redis 中,key 对应的 value 不存在。

此时思考一下,key 对应的 value 不存在,无非以下两种情况:

  • key 被主动删除
  • key 过期了

继续跟着代码往上走,发现前面执行了 setNx 命令,并且返回 setNxResult 表示是否成功。

正常来说,当 setNxResult 为 false 的时候,加锁失败,此时代码时不应该往下走的,但在本段代码中,却继续往下走!

问了下相关同事,说是为了做可重入锁......(弱弱吐槽下,可重入锁也不是这样干的啊...)

其实分析到这,已经可以知道是什么原因导致的异常故障了,即上面说的,key 被主动删除、key 过期导致。

下面假设有两个线程,对同一个 key 加锁,分别对应以上两种情况:

**①key 被主动删除的情况,**发生于分布式锁加锁逻辑执行完后,调用 unlock 方法,见以上 RedisLockAspect 类中 finally 部分,如下图:

**②key 过期的情况,**主要在线程加锁并设置过期时间后,执行业务代码耗费的时间超过设置的锁过期时间,并且在锁过期前,未对锁进行续期:

解决方案

从上面的代码看来,这已经不是简单的 Long.parseLong("null") 问题了,这是整个 Redis 分布式锁实现的问题。

并且该分布式锁在整个项目中大量使用,可想而知其实问题非常严重,如果只是解决 Long.parseLong("null") 的问题,无疑就是隔靴挠痒,没有任何意义的。

一般情况下,自定义 Redis 分布式锁容易出现以下几大问题:

  • setNx 锁释放问题
  • setNx Expire 原子性问题
  • 锁过期问题
  • 多线程释放锁问题
  • 可重入问题
  • 大量失败时自旋锁问题
  • 主从架构下锁数据同步问题

结合以上故障代码,可以发现项目中的 Redis 分布式锁实现几乎未对 Redis 分布式锁问题进行考虑。

以下为主要问题以及对应解决方案:

  • **setNx 和 expire 原子操作:**使用 Lua 脚本,在一次 Lua 脚本命令中,执行 setNx 与 expire 命令,保证原子性。
  • **锁过期问题:**为防止锁自动过期,可在锁过期前,定时对锁过期时间进行续期。
  • **可重入问题:**可重入设计粒度需到线程级别,可在锁上加上线程唯一 id。
  • **锁自旋问题:**参考 JDK 中 AQS 设计,实现获取锁时最大等待时长。

对于项目中的问题以及每个问题的解决方案实现,baidu 一下就有大量参考,此处不再介绍。

目前比较成熟的综合解决方案为使用 Redisson 客户端,以下为简单伪代码 demo:

public class RedisLockAspect{
@Autowired
private Redisson redisson;

public void around(ProceedingJoinPoint pjp) {
String key = "...";
Long waitTime = 3000L;
//获取锁
RLock lock = redisson.getLock(key);
boolean lockSuccess = false;
try {
//加锁设置超时时间,防止无限自旋。默认启用看门狗功能(自动对锁进行续期)
lockSuccess = lock.tryLock(waitTime);
//执行业务逻辑
pjp.proceed();
}finally {
//解锁,防止释放其他线程锁
if (lock.isLocked() && lock.isHeldByCurrentThread() && lockSuccess){
lock.unlock();
}
}
}
}

使用 Redisson 可以快速解决目前项目中 Redis 分布式锁存在的问题。除此之外,对于 Redis 主从架构下数据同步导致的锁问题,对应的解决方案 RedLock,也提供了相应的实现。

更多使用文档详见官方文档:

https://github.com/liulongbiao/redisson-doc-cn

总结

对于分布式锁来说,可实现方案其实远远不止 Redis 这个实现途径,比如基于 Zookeeper、基于 Etcd 等方案。

但其实对于目的来说,都是殊途同归,重点在于,如何安全、正确的使用这些方案,保证业务正常。

对于研发团队来说,针对类似的问题,需要对技术小伙伴进行培训,不断提升技术,更需要重视 codereview 工作,及时识别风险,避免发生故障造成严重损失(本次故障造成脏数据修复耗时一个多星期)。

敬畏技术,忠于业务。

文章目录
  1. 1. 背景
  2. 2. 问题分析
  3. 3. 解决方案
  4. 4. 总结