首页 > 其他分享 >分布式锁实现方式

分布式锁实现方式

时间:2024-10-30 14:18:09浏览次数:6  
标签:释放 加锁 方式 实现 lock 分布式 public 客户端

1. 基于数据库的分布式锁

实现原理
  • 加锁:在数据库表中创建一个记录来表示锁,通常是使用 INSERTUPDATE 语句完成。可以创建一个锁表,并在表中使用唯一的 ID 字段表示资源,锁被持有的标志可以使用时间戳或状态字段标记。
    • 方式 1:利用数据库的行锁(如 SELECT FOR UPDATE)。客户端尝试获取锁时,会查询该资源并使用行锁来锁定行。
    • 方式 2:使用唯一索引来防止重复插入。通过 INSERT INTO 操作,如果插入成功则表示加锁成功,若失败则说明锁已存在。
  • 释放锁:任务完成时,删除锁记录或更新锁状态字段。
场景
  • 适用于少量并发访问的场景。
  • 常见于简单的分布式系统中,因为数据库操作对开发人员来说较为熟悉,并且可以满足基本锁需求。
优缺点
  • 优点
    • 实现简单:无需引入额外组件,数据库在分布式系统中是常用的组件。
    • 强一致性:大多数关系数据库提供强一致性,确保锁的可靠性。
  • 缺点
    • 性能瓶颈:数据库锁的性能较差,特别在高并发场景中,事务开销大,扩展性有限。
    • 死锁风险:锁释放异常或未及时释放时,可能造成资源的死锁。
代码示例

假设使用 MySQL 数据库来实现锁。可以创建一张锁表,每当需要加锁时,往该表插入数据,并利用唯一索引确保只有一个客户端能加锁成功。

数据库表结构
CREATE TABLE distributed_lock (
    lock_key VARCHAR(255) PRIMARY KEY,
    lock_value VARCHAR(255),
    expire_time TIMESTAMP
);
Java 代码示例

使用 JDBC 进行数据库连接和操作。

import java.sql.*;
import java.time.LocalDateTime;

public class DatabaseDistributedLock {
    private static final String LOCK_KEY = "my_lock";
    private Connection connection;

    public DatabaseDistributedLock(Connection connection) {
        this.connection = connection;
    }

    public boolean acquireLock(String lockValue, int expireSeconds) throws SQLException {
        String sql = "INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?) " +
                     "ON DUPLICATE KEY UPDATE lock_value = IF(expire_time < NOW(), ?, lock_value), " +
                     "expire_time = IF(expire_time < NOW(), ?, expire_time)";
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, LOCK_KEY);
            statement.setString(2, lockValue);
            statement.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now().plusSeconds(expireSeconds)));
            statement.setString(4, lockValue);
            statement.setTimestamp(5, Timestamp.valueOf(LocalDateTime.now().plusSeconds(expireSeconds)));
            return statement.executeUpdate() > 0;
        }
    }

    public void releaseLock(String lockValue) throws SQLException {
        String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, LOCK_KEY);
            statement.setString(2, lockValue);
            statement.executeUpdate();
        }
    }
}

2. 基于 Redis 的分布式锁

Redis 具有单线程的特点,确保了操作的原子性,同时提供了丰富的命令和高性能,因此在分布式锁实现中广泛使用。

实现原理
  • 加锁:使用 Redis 提供的 SETNX 命令(SET if Not Exists),可以原子地设置锁键及其过期时间,如 SET key value NX EX seconds,其中 NX 表示仅当键不存在时才设置,EX 表示过期时间。
  • 锁续期:如果任务超出预期时间未完成,可以通过守护进程定期延长锁的有效期,以避免锁被误释放。
  • 释放锁:任务完成后,确保只由持有锁的客户端执行删除操作。通常使用 Lua 脚本保证操作原子性,比如对比当前锁值后再删除。
  • Redlock 算法:为高可用性,Redis 分布式锁常使用 Redlock 算法,通过在多个 Redis 实例上分别加锁来提高锁的可靠性和容错能力。
场景
  • 适合对并发和性能有较高要求的分布式系统。
  • 用于较短时间的锁场景,如防止数据重复提交、限流、库存管理等。
优缺点
  • 优点
    • 高性能:Redis 的内存操作速度快,能承受高并发请求。
    • 支持过期自动释放:锁自动过期释放,避免死锁。
    • 分布式支持:通过 Redlock 算法在多个 Redis 实例间实现高可用锁。
  • 缺点
    • 锁误释放风险:如果客户端长时间持有锁,可能在任务未完成时锁过期被其他客户端获取。
    • 数据一致性问题:Redis 默认是主从架构,可能存在主从数据同步延迟的问题。
代码示例

Redis 实现分布式锁的核心是 SETNX 命令。使用过期时间来确保锁在任务异常时能够自动释放。

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.Collections;
import java.util.UUID;

@Component
public class RedisDistributedLock {

    private final RedisTemplate<String, String> redisTemplate;
    private static final String LOCK_KEY = "my_lock"; // 锁的键名
    private static final Duration LOCK_EXPIRATION = Duration.ofSeconds(30); // 锁过期时间

    public RedisDistributedLock(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 尝试获取锁
     * @return 返回获取到的锁的唯一值,用于释放锁时进行校验
     */
    public String acquireLock() {
        String lockValue = UUID.randomUUID().toString(); // 唯一标识当前锁的值
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(LOCK_KEY, lockValue, LOCK_EXPIRATION); // 尝试加锁,并设置过期时间
        return Boolean.TRUE.equals(success) ? lockValue : null;
    }

    /**
     * 释放锁
     * @param lockValue 当前线程持有的锁的值
     * @return 是否成功释放锁
     */
    public boolean releaseLock(String lockValue) {
        // 使用 Lua 脚本,保证解锁过程的原子性
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                           "return redis.call('del', KEYS[1]) " +
                           "else return 0 end";

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(LOCK_KEY), lockValue);
        return result != null && result > 0;
    }
}
说明
  • acquireLock 方法使用 SETNXEX 实现加锁。
  • releaseLock 方法通过 Lua 脚本确保只释放当前持有的锁。

3. 基于 ZooKeeper 的分布式锁

ZooKeeper 是一个分布式协调服务,通过顺序节点和会话超时机制可以提供可靠的分布式锁。

实现原理
  • 加锁:客户端尝试在指定目录下创建一个临时顺序节点,每次加锁请求时,ZooKeeper 自动生成一个递增的序号,所有客户端都获取该目录下的节点列表。创建最小序号的节点即可获得锁,其他节点则监听比自己小的节点的删除事件。
  • 释放锁:持有锁的客户端完成任务后删除自己创建的节点。监听到锁释放事件的客户端可以重新获取锁。
  • 会话超时:ZooKeeper 提供会话超时机制,当客户端失联后,ZooKeeper 自动删除该客户端的临时节点,确保锁自动释放。
场景
  • 适用于一致性要求高、分布式事务较多的场景,如金融系统和分布式数据库。
  • 特别适合长时间锁的场景,ZooKeeper 的会话机制可保证锁在意外断开时自动释放。
优缺点
  • 优点
    • 高可靠性:ZooKeeper 天然支持分布式一致性,适合多节点环境。
    • 自动释放:通过会话机制避免死锁风险。
    • 锁可见性:可以轻松实现分布式队列、选主等功能。
  • 缺点
    • 较高延迟:ZooKeeper 使用磁盘保存节点信息,相较 Redis 性能略低。
    • 复杂性较高:需要专门运维 ZooKeeper 集群,同时客户端与 ZooKeeper 需要保持心跳,增加了复杂度。
代码示例

ZooKeeper 使用临时节点实现锁,每个客户端创建临时顺序节点,通过判断最小节点来决定锁的持有者。

使用 Curator 框架:

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;

public class ZookeeperDistributedLock {
    private final InterProcessMutex lock;
    private static final String LOCK_PATH = "/distributed_lock/my_lock";

    public ZookeeperDistributedLock(CuratorFramework client) {
        this.lock = new InterProcessMutex(client, LOCK_PATH);
    }

    public boolean acquireLock() {
        try {
            lock.acquire();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public void releaseLock() {
        try {
            if (lock.isAcquiredInThisProcess()) {
                lock.release();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
说明
  • Curator 库提供了 InterProcessMutex,简化了分布式锁的实现。
  • acquireLockreleaseLock 方法分别用于加锁和释放锁,通过会话超时机制实现锁自动释放。

对比总结

实现方式优点缺点适用场景
数据库简单实现,适用小规模系统性能较低,易造成死锁低频、小规模分布式锁需求
Redis高性能、自动过期、支持 Redlock 算法锁误释放风险,数据一致性受主从架构影响高并发、高性能场景
ZooKeeper高可靠性、自动释放,支持选主和分布式队列性能略低,复杂性高长时间锁和一致性要求高场景

标签:释放,加锁,方式,实现,lock,分布式,public,客户端
From: https://blog.csdn.net/u012108607/article/details/143363485

相关文章