共400行

Java锁vs redis锁

2026-05-08 13:09:43

Java锁 vs redis锁

Java锁(单机锁)和Redis锁(分布式锁)核心区别:Java锁只管用在同一JVM内多线程互斥;Redis锁用于跨机器/跨服务的分布式互斥。下面从原理、特性、场景、代码示例与选型做清晰对比。


一、核心原理与定位


二、关键特性对比(速览)

特性 Java锁 Redis锁
适用范围 单机JVM内多线程 跨机器/跨服务分布式
性能 极高(内存操作,纳秒/微秒级) 高(网络+Redis,毫秒级)
可靠性 无单点问题 依赖Redis;主从/Redlock提升可靠
死锁规避 无自动过期;需代码控制 过期时间+续期;自动释放
可重入 原生支持(synchronized/ReentrantLock) Redisson支持;原生需手动
阻塞/等待 原生阻塞/唤醒 需自旋/订阅;Redisson封装
部署成本 无额外组件 需Redis(集群更稳)

三、优缺点

Java锁

Redis锁


四、典型场景


五、代码示例

Java锁(ReentrantLock)

private static final ReentrantLock lock=new ReentrantLock();

public void doSomething() {
    lock.lock(); // 加锁
    try {
        // 临界区:同一JVM内仅一线程执行
    } finally {
        lock.unlock(); // 释放
    }
}

Redis锁(原生SETNX+过期)

String key="lock:product:1001";
// NX:不存在才设;EX:10秒过期
boolean ok=redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
if (ok) {
    try {
        // 分布式临界区:多服务仅一个执行
    } finally {
        redisTemplate.delete(key); // 释放
    }
}

Redis锁(Redisson 可重入+看门狗)

RLock lock=redissonClient.getLock("lock:product:1001");
lock.lock(); // 看门狗自动续期(默认30秒)
try {
    // 业务
} finally {
    lock.unlock();
}

六、选型建议


Java锁和Redis锁的常见踩坑点有哪些?

一、Java 本地锁(synchronized / ReentrantLock)踩坑

1. 分布式环境完全失效

问题
synchronizedReentrantLockJVM 级锁,只在当前服务实例内生效。
多实例集群、微服务部署时,每个服务各自加锁,互不干涉,并发穿透、超卖、重复执行
场景:单机测试正常,上集群直接崩。
解决:分布式场景必须用分布式锁(Redis/Zookeeper)。

2. 锁粒度太大/太小

3. 忘记在 finally 释放锁

问题
ReentrantLock 业务异常直接抛错,不走 unlock()锁永久持有,线程死锁、接口卡死

// 错误写法
lock.lock();
// 业务代码异常 → 无法解锁
lock.unlock();

规范lock() 后必须 try-finally 解锁。

4. 加锁顺序不一致导致死锁

多把锁嵌套获取时,不同线程获取顺序相反,互相等待。
解决:统一锁的获取顺序、设置超时锁 tryLock(time)

5. 锁对象被修改/替换导致锁失效

// 错误:字符串重新赋值,锁对象变了
private String lockObj = "lock";
synchronized (lockObj) {
    lockObj = "newLock"; 
}

原因:锁依赖对象监视器,对象变了锁就彻底无效。
解决:用 final 修饰锁对象。

6. 乐观锁、悲观锁选错

简单计数、本地缓存更新强行加悲观锁,浪费性能;
高竞争、强一致性场景乱用 CAS 乐观锁,导致自旋空转、CPU 飙高


二、Redis 分布式锁 高频踩坑(重点)

1. 加锁和设过期时间非原子,导致死锁

错误写法

redis.set(key,val);
redis.expire(key,10);

两行命令非原子,若中间服务宕机,key 永久存在,死锁
正确:使用原子命令 SET NX EX 或 Lua 脚本。

2. 锁被别的业务强行删除(误删锁)

问题
A 加锁,业务执行超时,锁自动过期释放;
B 正常抢到锁;
A 执行完直接 delete key把 B 的锁删了,并发安全失效。

解决

3. 业务执行超时,锁自动过期释放

业务耗时 > 锁过期时间,锁提前失效,多个服务同时进入临界区,超卖/重复下单。

解决方案

  1. 手动预估业务时间,拉长过期时间;
  2. Redisson 看门狗(自动续期),长任务首选。

4. 主从架构锁失效(主从延迟问题)

问题
Redis 主从、哨兵、集群模式:

  1. 主节点加锁成功,数据还未同步到从节点;
  2. 主节点立刻宕机,从节点升级为主;
  3. 新主库无锁 Key,其他服务再次加锁成功,锁失效

解决

5. 自旋抢锁导致 CPU、Redis 压力暴增

手写 Redis 锁用 while + 循环 setNx 死循环抢锁,
大量无效请求打满 Redis,本地 CPU 飙升。

解决

6. 不支持可重入,自身锁住自己

同一服务同一线程,重复加锁直接死锁。
原因:原生 Redis 锁只是 Key 维度互斥,不识别线程。
解决:直接用 Redisson 可重入锁。

7. 解锁逻辑未原子化

判断 key 是否存在、删除 key 分两步,并发下仍会误删别人锁。
正确:统一用 Lua 脚本实现「判断+删除」原子操作。

8. 锁过期时间设置不合理

9. 集群路由、key 哈希槽问题

Redis Cluster 下,锁 key 分散在不同槽位,
若业务多锁跨槽位,增加锁冲突与异常概率。
优化:关联资源的锁统一用相同 hashTag,固定槽位。


三、终极选型避坑总结

  1. 单机服务 → 优先 Java 本地锁:性能高、无依赖、简单稳定。
  2. 集群/微服务/分布式 → 禁止用本地锁,必用分布式锁。
  3. 普通并发、允许弱一致 → Redisson Redis 锁(最优解,封装完所有坑)。
  4. 金融、订单、强一致性 → 放弃 Redis 锁,用 Zookeeper 锁/数据库悲观锁。

四、极简落地规范

  1. 本地锁:final 锁对象 + try-finally 必释放。
  2. Redis 锁:

安全版Redis分布式锁的代码模板

下面给你两套生产级安全 Redis 分布式锁模板

  1. 原生手写版(SET NX EX + Lua 解锁,无第三方依赖)
  2. Redisson 最简安全版(企业常用,解决续期、可重入、阻塞)

一、原生安全版(SpringBoot + RedisTemplate)

核心保障

  1. 加锁原子:setIfAbsent(key, uuid, expire, timeUnit)
  2. 防误删:存唯一 UUID,解锁先校验持有者
  3. 解锁原子:Lua 脚本判断+删除,杜绝并发误删
  4. 必释放:try-finally 保证解锁
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
public class RedisDistributedLock {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    // 锁过期时间,根据业务调整
    private static final long LOCK_EXPIRE_TIME = 30;
    private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;

    // 解锁Lua脚本:判断value一致才删除
    private static final String UNLOCK_LUA_SCRIPT =
            "if redis.call('get',KEYS[1]) == ARGV[1] then " +
            "return redis.call('del',KEYS[1]) " +
            "else return 0 end";

    /**
     * 加锁
     * @param lockKey 锁标识
     * @return 成功返回唯一标识,失败返回null
     */
    public String lock(String lockKey) {
        // 唯一标识,防止误删
        String lockValue = UUID.randomUUID().toString();
        // NX:不存在才上锁  EX:自动过期
        boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, LOCK_EXPIRE_TIME, TIME_UNIT);
        return success ? lockValue : null;
    }

    /**
     * 解锁
     * @param lockKey 锁标识
     * @param lockValue 加锁时返回的唯一标识
     */
    public void unlock(String lockKey, String lockValue) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(UNLOCK_LUA_SCRIPT);
        script.setResultType(Long.class);
        // 原子执行:校验+删除
        redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue);
    }
}

使用示例

@Service
public class BusinessService {

    @Resource
    private RedisDistributedLock redisDistributedLock;

    public void seckillGoods(Long goodsId) {
        String lockKey = "lock:goods:" + goodsId;
        String lockValue = null;
        try {
            // 尝试加锁
            lockValue = redisDistributedLock.lock(lockKey);
            if (lockValue == null) {
                // 加锁失败,直接返回/重试
                throw new RuntimeException("操作频繁,请稍后再试");
            }
            // --------------- 临界区业务 ---------------
            // 库存扣减、订单创建、防重复提交等
            // -----------------------------------------
        } finally {
            // 必须解锁
            if (lockValue != null) {
                redisDistributedLock.unlock(lockKey, lockValue);
            }
        }
    }
}

二、Redisson 终极安全版(推荐生产使用)

自动解决:

1. 引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.5</version>
</dependency>

2. 极简使用

@Service
public class RedissonLockService {

    @Resource
    private RedissonClient redissonClient;

    public void doBiz(Long resourceId) {
        String lockKey = "lock:biz:" + resourceId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 无参lock:看门狗续期,适合长耗时业务
            lock.lock();

            // --------------- 业务代码 ---------------

        } finally {
            // 释放锁
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

常用加锁方式

// 1. 阻塞加锁,看门狗自动续期(常用)
lock.lock();

// 2. 尝试获取锁,等待5秒,锁15秒自动过期
boolean tryLock = lock.tryLock(5, 15, TimeUnit.SECONDS);

三、关键避坑总结(对应上面代码)

  1. ❌ 禁止:set + expire 分开写 → ✅ 原子 setIfAbsent NX EX
  2. ❌ 禁止:直接 delete 删锁 → ✅ Lua 校验 value 再删除
  3. ❌ 禁止:不设置过期时间 → ✅ 强制过期防死锁
  4. ❌ 长业务过期 → ✅ 用 Redisson 看门狗
  5. ❌ 不加唯一标识 → ✅ UUID 防止跨线程误删