java中如何使用redis实现分布式锁

在Java中使用Redis实现分布式锁主要有两种方式:一种是使用Redisson客户端库,另一种是使用Jedis/Lettuce配合Lua脚本。下面分别介绍这两种实现方式:

1. 使用Redisson实现(推荐)

Redisson是一个成熟的Redis客户端,内置了分布式锁的实现。

1.1 添加依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.17.0</version>
</dependency>

1.2 配置Redisson客户端

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonConfig {
    public static RedissonClient getRedissonClient() {
        Config config = new Config();
        // 单节点模式
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setPassword("your_password")
              .setDatabase(0);
        
        // 集群模式
        // config.useClusterServers()
        //       .addNodeAddress("redis://127.0.0.1:7001", "redis://127.0.0.1:7002");
        
        return Redisson.create(config);
    }
}

1.3 使用分布式锁

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;

public class DistributedLockWithRedisson {
    private final RedissonClient redissonClient;
    
    public DistributedLockWithRedisson(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
    
    /**
     * 尝试获取锁
     */
    public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    /**
     * 释放锁
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
    
    /**
     * 示例:使用锁执行业务逻辑
     */
    public void executeWithLock(String lockKey, Runnable task) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,最多等待10秒,锁持有时间30秒
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (locked) {
                task.run();
            } else {
                throw new RuntimeException("获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断", e);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    /**
     * 可重入锁示例
     */
    public void reentrantLockExample() {
        String lockKey = "order:123";
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 第一次加锁
            lock.lock();
            
            // 在同一个线程中再次获取锁(可重入)
            lock.lock();
            try {
                // 执行业务逻辑
                System.out.println("执行业务逻辑");
            } finally {
                lock.unlock(); // 释放内层锁
            }
        } finally {
            lock.unlock(); // 释放外层锁
        }
    }
}

2. 使用Jedis + Lua脚本实现

2.1 添加依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.1</version>
</dependency>

2.2 实现分布式锁工具类

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.UUID;

public class RedisDistributedLock {
    private final JedisPool jedisPool;
    
    public RedisDistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }
    
    // 解锁Lua脚本
    private static final String UNLOCK_SCRIPT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";
    
    // 续期Lua脚本
    private static final String RENEW_SCRIPT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('pexpire', KEYS[1], ARGV[2]) " +
        "else " +
        "   return 0 " +
        "end";
    
    /**
     * 尝试获取锁
     * @param lockKey 锁的key
     * @param requestId 请求标识(可以使用UUID)
     * @param expireTime 过期时间(毫秒)
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        try (Jedis jedis = jedisPool.getResource()) {
            SetParams params = SetParams.setParams()
                .nx()  // 仅当key不存在时设置
                .px(expireTime);  // 设置过期时间(毫秒)
            
            String result = jedis.set(lockKey, requestId, params);
            return "OK".equals(result);
        }
    }
    
    /**
     * 阻塞获取锁(自旋)
     */
    public boolean lock(String lockKey, String requestId, 
                       long expireTime, long timeout) throws InterruptedException {
        long endTime = System.currentTimeMillis() + timeout;
        
        while (System.currentTimeMillis() < endTime) {
            if (tryLock(lockKey, requestId, expireTime)) {
                return true;
            }
            Thread.sleep(100); // 等待100ms后重试
        }
        return false;
    }
    
    /**
     * 释放锁
     */
    public boolean unlock(String lockKey, String requestId) {
        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(UNLOCK_SCRIPT, 
                Collections.singletonList(lockKey), 
                Collections.singletonList(requestId));
            return Long.valueOf(1L).equals(result);
        }
    }
    
    /**
     * 锁续期
     */
    public boolean renewLock(String lockKey, String requestId, long expireTime) {
        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(RENEW_SCRIPT, 
                Collections.singletonList(lockKey), 
                Arrays.asList(requestId, String.valueOf(expireTime)));
            return Long.valueOf(1L).equals(result);
        }
    }
    
    /**
     * 使用示例
     */
    public void example() {
        String lockKey = "order:process:123";
        String requestId = UUID.randomUUID().toString();
        
        try {
            // 尝试获取锁,过期时间30秒
            if (tryLock(lockKey, requestId, 30000)) {
                try {
                    // 执行业务逻辑
                    System.out.println("获取锁成功,执行业务逻辑");
                    
                    // 如果需要,可以启动一个守护线程进行锁续期
                    startRenewalThread(lockKey, requestId, 30000);
                    
                } finally {
                    // 释放锁
                    unlock(lockKey, requestId);
                }
            } else {
                System.out.println("获取锁失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 启动锁续期线程
     */
    private void startRenewalThread(String lockKey, String requestId, long expireTime) {
        Thread renewalThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(expireTime / 3); // 每过期时间的1/3续期一次
                    if (!renewLock(lockKey, requestId, expireTime)) {
                        break; // 续期失败,退出
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        renewalThread.setDaemon(true);
        renewalThread.start();
    }
}

3. 使用Spring Boot + Lettuce实现

3.1 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 配置类

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
    
    @Bean
    public RedisLockHelper redisLockHelper(RedisTemplate<String, Object> redisTemplate) {
        return new RedisLockHelper(redisTemplate);
    }
}

3.3 分布式锁工具类

@Component
public class RedisLockHelper {
    private final RedisTemplate<String, Object> redisTemplate;
    
    public RedisLockHelper(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    private static final DefaultRedisScript<Long> RENEW_SCRIPT;
    
    static {
        // 初始化解锁脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setScriptText(
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end");
        UNLOCK_SCRIPT.setResultType(Long.class);
        
        // 初始化续期脚本
        RENEW_SCRIPT = new DefaultRedisScript<>();
        RENEW_SCRIPT.setScriptText(
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('pexpire', KEYS[1], ARGV[2]) " +
            "else " +
            "   return 0 " +
            "end");
        RENEW_SCRIPT.setResultType(Long.class);
    }
    
    /**
     * 获取锁
     */
    public boolean acquireLock(String lockKey, String requestId, long expireMillis) {
        return Boolean.TRUE.equals(redisTemplate.execute(
            (RedisConnection connection) -> 
                connection.set(
                    lockKey.getBytes(),
                    requestId.getBytes(),
                    Expiration.milliseconds(expireMillis),
                    RedisStringCommands.SetOption.SET_IF_ABSENT
                )
        ));
    }
    
    /**
     * 释放锁
     */
    public boolean releaseLock(String lockKey, String requestId) {
        Long result = redisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(lockKey),
            requestId
        );
        return result != null && result == 1;
    }
    
    /**
     * 自动重试获取锁
     */
    public boolean tryLockWithRetry(String lockKey, String requestId, 
                                   long expireMillis, int maxRetry, 
                                   long retryInterval) throws InterruptedException {
        for (int i = 0; i < maxRetry; i++) {
            if (acquireLock(lockKey, requestId, expireMillis)) {
                return true;
            }
            Thread.sleep(retryInterval);
        }
        return false;
    }
}

4. 最佳实践和注意事项

4.1 关键要点

  1. 原子性操作:使用SET NX PX命令或Lua脚本保证原子性
  2. 唯一标识:每个锁的value使用唯一ID(如UUID),避免误删
  3. 设置过期时间:防止死锁
  4. 锁续期机制:对于长任务,需要续期
  5. 可重入性:考虑是否支持同一线程重入

4.2 使用模板

@Component
public class DistributedLockService {
    
    @Autowired
    private RedisLockHelper redisLockHelper;
    
    /**
     * 分布式锁执行模板
     */
    public <T> T executeWithLock(String lockKey, long expireMillis, 
                                Callable<T> task) {
        String requestId = UUID.randomUUID().toString();
        boolean locked = false;
        
        try {
            // 尝试获取锁
            locked = redisLockHelper.acquireLock(lockKey, requestId, expireMillis);
            if (!locked) {
                throw new RuntimeException("获取分布式锁失败");
            }
            
            // 执行业务逻辑
            return task.call();
            
        } catch (Exception e) {
            throw new RuntimeException("执行业务逻辑失败", e);
        } finally {
            if (locked) {
                // 释放锁
                redisLockHelper.releaseLock(lockKey, requestId);
            }
        }
    }
    
    /**
     * 使用示例
     */
    public void processOrder(String orderId) {
        String lockKey = "lock:order:process:" + orderId;
        
        executeWithLock(lockKey, 30000, () -> {
            // 业务逻辑
            System.out.println("处理订单: " + orderId);
            return null;
        });
    }
}

总结

  1. Redisson:功能最全,支持可重入锁、公平锁、读写锁、红锁等,内置看门狗自动续期
  2. Jedis/Lettuce + Lua:更轻量,控制更灵活,但需要自己实现更多功能
  3. 选择建议
    • 如果项目复杂,需要多种锁类型,推荐使用Redisson
    • 如果只是简单场景,可以使用Jedis/Lettuce实现
    • Spring Boot项目可以使用Spring Data Redis

分布式锁的实现需要考虑:原子性、死锁预防、锁续期、可重入性、性能等因素。


作 者:南烛
链 接:https://www.itnotes.top/archives/963
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇