分布式锁:5个案例,从入门到入土

什么是分布式锁

分布式锁是用于解决多个进程/线程在分布式环境下访问共享资源的并发控制问题的一种技术。在单进程/线程的环境下,可以通过语言提供的锁机制,例如Java中的synchronized关键字或者ReentrantLock类,来保证资源的互斥访问。但是在分布式环境中,多个进程/线程需要共享同一份数据,这时就需要分布式锁来保证数据的正确性。

为什么需要分布式锁

在分布式环境中,多个进程/线程同时访问共享数据时可能会出现以下问题:

数据丢失:由于多个进程/线程同时写入数据,可能会导致数据丢失。

数据不一致:由于多个进程/线程同时读取数据,其中某些进程/线程读取到的是旧数据,导致数据不一致。

死锁:由于多个进程/线程同时持有相同的锁而陷入死锁状态。

因此,分布式锁被广泛应用于分布式环境中。

分布式锁的实现方式

目前,分布式锁的实现方式主要有以下几种:

1. 基于数据库

使用数据库实现分布式锁的思路比较简单,就是利用数据库的唯一性约束来实现资源的互斥访问。具体实现方法是,在数据库中创建一张表,将数据的唯一标识作为表的主键,并为该字段创建唯一性约束,当一个进程/线程需要访问资源时,向数据库中插入一条记录,如果插入成功,则获取到锁,否则说明锁已被其他进程/线程占用。

下面是使用MySQL数据库实现分布式锁的Java代码:

public class MysqlDistributedLock implements DistributedLock {

private JdbcTemplate jdbcTemplate;

public MysqlDistributedLock(DataSource dataSource) {

this.jdbcTemplate = new JdbcTemplate(dataSource);

}

@Override

public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {

long nanoTimeout = timeUnit.toNanos(timeout);

long start = System.nanoTime();

do {

try {

jdbcTemplate.execute("INSERT INTO distributed_lock (key) VALUES ('" + key + "')");

return true;

} catch (DuplicateKeyException e) {

// key已存在,锁已被占用

} catch (Exception e) {

// 锁获取失败

return false;

}

if (System.nanoTime() - start > nanoTimeout) {

// 获取锁超时

return false;

}

// 休眠1毫秒

try {

Thread.sleep(1);

} catch (InterruptedException e) {

// ignore

}

} while (true);

}

@Override

public void unlock(String key) {

jdbcTemplate.execute("DELETE FROM distributed_lock WHERE key='" + key + "'");

}

}

2. 基于Redis

Redis是一个高性能的键值存储数据库,支持多种数据结构和丰富的命令集,被广泛应用于分布式环境中。通过Redis实现分布式锁的思路是使用Redis的SETNX命令来实现锁的占用,使用DEL命令来释放锁。具体实现方法是,在Redis中用一个key作为锁的标识,当一个进程/线程需要访问资源时,使用SETNX命令尝试将该key的值设置为1,如果设置成功,则获取到锁,并设置一个过期时间,否则说明锁已被其他进程/线程占用。

下面是使用Redis实现分布式锁的Java代码:

public class RedisDistributedLock implements DistributedLock {

private Jedis jedis;

public RedisDistributedLock(Jedis jedis) {

this.jedis = jedis;

}

@Override

public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {

String status = jedis.set(key, "1", "NX", "EX", timeUnit.toSeconds(timeout));

return "OK".equals(status);

}

@Override

public void unlock(String key) {

jedis.del(key);

}

}

3. 基于ZooKeeper

ZooKeeper是一个分布式协调服务,提供了高可用、高可靠、有序的数据访问能力。通过ZooKeeper实现分布式锁的思路是使用ZooKeeper的Node永久有序节点(Persistent Sequential Node)来实现锁的占用,使用deleteNode命令来释放锁。具体实现方法是,创建一个永久有序节点,每个进程/线程依次尝试创建节点,如果创建节点成功,则获取到锁,否则监听它前一个节点的删除事件,等待前一个节点被删除后再次尝试创建节点。

下面是使用ZooKeeper实现分布式锁的Java代码:

public class ZooKeeperDistributedLock implements DistributedLock {

private ZooKeeper zk;

private String lockNode;

private String myNode;

public ZooKeeperDistributedLock(ZooKeeper zk, String lockNode) {

this.zk = zk;

this.lockNode = lockNode;

}

@Override

public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {

try {

myNode = zk.create(lockNode + "/lock-", null, OPEN_ACL_UNSAFE, EPHEMERAL_SEQUENTIAL);

while (true) {

List<String> nodes = zk.getChildren(lockNode, false);

Collections.sort(nodes);

int index = nodes.indexOf(myNode.substring(lockNode.length() + 1));

if (index == 0) {

// I am the owner of the lock

return true;

} else {

String lastNode = nodes.get(index - 1);

CountDownLatch latch = new CountDownLatch(1);

Stat stat = zk.exists(lockNode + "/" + lastNode, new Watcher() {

@Override

public void process(WatchedEvent event) {

latch.countDown();

}

});

// 检查是否是第一个节点的watcher

if (stat != null && latch.await(timeout, timeUnit)) {

// 等待超时

return false;

}

}

}

} catch (KeeperException | InterruptedException e) {

return false;

}

}

@Override

public void unlock(String key) {

try {

zk.delete(myNode, -1);

} catch (KeeperException | InterruptedException e) {

// handle exception

}

}

}

4. 基于Redisson

Redisson是一个基于Redis实现的分布式Java对象和服务框架,提供了丰富的分布式应用开发组件。通过Redisson实现分布式锁的思路是使用Redisson的分布式锁对象RLock来实现锁的占用。具体实现方法是,在Redisson中使用getLock方法获取锁对象,调用lock方法获取锁,在不需要锁时调用unlock方法释放锁。

下面是使用Redisson实现分布式锁的Java代码:

public class RedissonDistributedLock implements DistributedLock {

private RedissonClient redisson;

private RLock lock;

public RedissonDistributedLock(RedissonClient redisson, String key) {

this.redisson = redisson;

this.lock = redisson.getLock(key);

}

@Override

public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {

try {

return lock.tryLock(timeout, timeUnit);

} catch (InterruptedException e) {

return false;

}

}

@Override

public void unlock(String key) {

lock.unlock();

}

}

5. 基于乐观锁

乐观锁的思路是,不使用互斥锁,而是每次操作时都尝试更新数据,如果更新成功,则说明获取到了锁,否则说明锁已被其他进程/线程占用。乐观锁相比互斥锁的优势是能够避免死锁和长时间阻塞,但是需要支持CAS(Compare And Swap)操作。

下面是使用乐观锁实现分布式锁的Java代码:

public class OptimisticDistributedLock implements DistributedLock {

private ConcurrentHashMap<String, Integer> locks = new ConcurrentHashMap<>();

@Override

public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {

long nanoTimeout = timeUnit.toNanos(timeout);

long start = System.nanoTime();

do {

Integer oldValue = locks.putIfAbsent(key, 1);

if (oldValue == null) {

// 获取锁成功

return true;

} else {

// 延迟一段时间再尝试获取锁

LockSupport.parkNanos(1000 * 1000);

}

} while (System.nanoTime() - start < nanoTimeout);

// 获取锁超时

return false;

}

@Override

public void unlock(String key) {

locks.remove(key);

}

}

分布式锁的应用场景

分布式锁被广泛应用于分布式环境中,常见的应用场景有:

缓存同步:多个节点同时访问一个缓存,需要使用分布式锁来保证数据的一致性。

限流:通过限制并发访问量来保护服务的稳定性,需要使用分布式锁来控制并发访问数量。

任务调度:多个节点同时调度一个任务,需要使用分布式锁来避免任务重复执行。

唯一数值生成:多个节点同时生成唯一数值,需要使用分布式锁来保证数值的唯一性。

在实际应用中,根据具体需求选择合适的实现方式和锁粒度是非常重要的。

分布式锁的局限和注意事项

1. 性能和可靠性

分布式锁的性能和可靠性会受到很多因素的影响,例如网络延迟、节点故障等。因此,在设计分布式锁时需要综合考虑这些因素,并对锁进行必要的优化和测试。

2. 锁粒度

锁粒度是指锁的范围大小,包括行级锁、表级锁、分片锁等。选择合适的锁粒度可以提高系统的并发性和可用性。

3. 死锁和活锁

由于分布式锁涉及到多个进程/线程的协作,容易出现死锁和活锁问题。死锁是指多个进程/线程相互等待,陷入无限等待的状态;活锁是指多个进程/线程不断重试,却无法获取到锁的状态。在设计分布式锁时需要注意避免死锁和活锁问题。

4. 容错和容灾

分布式锁应考虑到容错和容灾机制,以提高系统的可用性。例如,在使用基于ZooKeeper实现的分布式锁时,需要注意ZooKeeper集群的高可用和自动故障转移机制。

总结

分布式锁是一种用于解决分布式环境下共享资源的并发控制问题的技术,常见的实现方式包括基于数据库、Redis、ZooKeeper、Redisson和乐观锁。在实际应用中,需要根据具体需求选择合适的实现方式和锁粒度,并注意避免死锁和活锁问题,同时考虑容错和容灾机制,以提高系统的可用性。

后端开发标签