搞定Redis分布式锁|实战(注解工具)

作为经历过618大促奋战的牛马,我曾亲眼见过分布式锁没处理好导致的百万级资损。今天咱们来聊聊Redis分布式锁,最后做成注解工具,方便使用!


一、先看个案例

场景:社区团购秒杀芒果,库存1000件,结果卖出1200件
代码

public void reduceStock(String productId) {
    // 查库存
    int stock = getStock(productId); 
    if(stock > 0){
        // 扣库存
        updateStock(productId, stock-1);
    }
}

问题:当100个请求同时查到库存=1时,全部执行扣减,导致超卖200件!


二、分布式锁的四大金刚

1. 互斥性(最基本)

  • 同一时刻只能有一个客户端持有锁

2. 防死锁(必须做到)

// 错误示范(没有超时时间)
redis.set("lock", "1", "NX"); 

// 正确姿势(原子操作设置过期时间)
redis.set("lock", "1", "NX", "EX", 30);

3. 谁加的锁谁解(关键细节)

// 错误示范(直接删除)
redis.del("lock"); 

// 正确姿势(校验身份)
if(redis.get("lock").equals(myId)){
    redis.del("lock");
}

4. 高可用(生产必备)

  • 单节点Redis宕机怎么办?
  • 集群方案:Redis Sentinel 或 Cluster

三、redis锁代码(带自动续命)

public class RedisLock {
    private final JedisPool jedisPool;
    private final String lockKey;
    private final String clientId = UUID.randomUUID().toString();
    private volatile boolean isLocked = false;
    private ScheduledExecutorService renewExecutor;

    public RedisLock(JedisPool jedisPool, String lockKey) {
        this.jedisPool = jedisPool;
        this.lockKey = lockKey;
    }

    /**
     * 加锁(支持超时和重试)
     */
    public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
        long start = System.currentTimeMillis();
        long maxWaitMillis = unit.toMillis(waitTime);

        while (System.currentTimeMillis() - start < maxWaitMillis) {
            try (Jedis jedis = jedisPool.getResource()) {
                // 核心加锁命令
                String result = jedis.set(lockKey, clientId, SetParams.setParams().nx().ex(30));
                if ("OK".equals(result)) {
                    isLocked = true;
                    startRenewal(); // 启动续命线程
                    return true;
                }
            }
            // 随机等待避免惊群效应
            Thread.sleep(50 + new Random().nextInt(50));
        }
        return false;
    }

    /**
     * 自动续期(保活机制)
     */
    private void startRenewal() {
        renewExecutor = Executors.newSingleThreadScheduledExecutor();
        renewExecutor.scheduleAtFixedRate(() -> {
            try (Jedis jedis = jedisPool.getResource()) {
                if (isLocked && clientId.equals(jedis.get(lockKey))) {
                    jedis.expire(lockKey, 30); // 续期30秒
                }
            }
        }, 10, 10, TimeUnit.SECONDS); // 每10秒检查一次
    }

    /**
     * 解锁(Lua脚本保证原子性)
     */
    public void unlock() {
        if (!isLocked) return;

        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        
        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(script, 
                Collections.singletonList(lockKey), 
                Collections.singletonList(clientId));
            
            if (result.equals(1L)) {
                stopRenewal();
                isLocked = false;
            }
        }
    }

    private void stopRenewal() {
        if (renewExecutor != null) {
            renewExecutor.shutdown();
        }
    }
}

四、代码提问

1. 加锁为什么要用UUID?

  • 防止其他客户端误删锁(比如A客户端超时后,B获取锁,A这时不能删B的锁)

2. 自动续期有什么用?

  • 解决业务执行时间超过锁过期时间的问题
  • 示例:设置30秒过期,每10秒续期一次

3. 为什么要用Lua脚本解锁?

  • 保证判断持有者删除锁的原子性
  • 防止执行到中间步骤时锁过期

五、测试大法

场景1:并发抢购测试

@Test
void testConcurrentLock() throws InterruptedException {
    int threadCount = 100;
    ExecutorService pool = Executors.newFixedThreadPool(threadCount);
    AtomicInteger successCount = new AtomicInteger();

    for (int i = 0; i < threadCount; i++) {
        pool.submit(() -> {
            try (Jedis jedis = jedisPool.getResource()) {
                RedisLock lock = new RedisLock(jedisPool, "test_lock");
                if (lock.tryLock(3, TimeUnit.SECONDS)) {
                    successCount.incrementAndGet();
                    Thread.sleep(100); // 模拟业务处理
                    lock.unlock();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    pool.shutdown();
    pool.awaitTermination(5, TimeUnit.SECONDS);
    assertEquals(1, successCount.get());
}

场景2:网络闪断测试

@Test
void testNetworkJitter() {
    RedisLock lock = new RedisLock(jedisPool, "network_test");
    
    // 模拟获取锁后Redis宕机
    lock.tryLock(1, TimeUnit.SECONDS);
    jedisPool.getResource().shutdown(); // 强制断开连接
    
    // 业务处理中...
    assertThrows(Exception.class, () -> {
        lock.unlock(); // 应该抛出连接异常
    });
}

六、注解式分布式锁工具

1. 定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLockable {
    String key();          // 锁的key
    int waitTime() default 3;   // 等待时间(秒)
    int leaseTime() default 30; // 持有时间(秒)
}

2. AOP切面实现

@Aspect
@Component
public class RedisLockAspect {
    @Autowired
    private JedisPool jedisPool;

    @Around("@annotation(lockable)")
    public Object around(ProceedingJoinPoint joinPoint, RedisLockable lockable) throws Throwable {
        String lockKey = parseKey(joinPoint, lockable.key());
        
        RedisLock lock = new RedisLock(jedisPool, lockKey);
        try {
            if (!lock.tryLock(lockable.waitTime(), TimeUnit.SECONDS)) {
                throw new RuntimeException("获取锁失败");
            }
            return joinPoint.proceed();
        } finally {
            lock.unlock();
        }
    }

    // 解析SpEL表达式(支持动态key)
    private String parseKey(ProceedingJoinPoint joinPoint, String keyExpr) {
        // 实现略,可用Spring Expression解析
        return keyExpr; 
    }
}

3. 使用示例

@Service
public class OrderService {
    
    @RedisLockable(key = "'order_lock:' + #orderId", 
                  waitTime = 5, 
                  leaseTime = 60)
    public void createOrder(String orderId) {
        // 业务逻辑
    }
}

七、踩坑预警

坑1:锁提前过期导致数据混乱

症状:日志显示两个线程同时进入临界区
解法

  • 合理设置leaseTime(建议业务耗时的2-3倍)
  • 启用健康检查自动续期

坑2:解锁失败导致死锁

症状:监控显示锁TTL一直刷新
解法

// 在finally中强制解锁(慎用!)
finally {
    if(lock.isLocked()){
        log.warn("强制解锁");
        lock.forceUnlock();
    }
}

坑3:集群脑裂导致双主

症状:两个节点同时持有锁
解法

  • 使用Redlock算法(需要至少3个主节点)
  • 设置min-slaves-to-write 1(Redis配置)

八、结 Checklist

当你实现分布式锁时,请对照检查:

  • 是否设置唯一客户端标识
  • 是否具备自动续期机制
  • 解锁操作是否原子性
  • 是否处理了网络分区问题
  • 是否有完善的监控(获取次数、等待时间等)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dami_king

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值