常见分布式锁方案

Posted by mjTree on April 7, 2024

更新于:2024-04-07 22:10

一、分布式锁常见需求

锁的功能在于确保资源的合理竞争,而分布式锁则是为了处理不同服务器之间进程对资源的竞争情况。功能场景有避免重复操作(订单多次支付)、防止并发竞态条件(账户进行余额扣除)、保证数据一致性(控制业务顺序流程)、定时任务调度(执行一次)等等。这里我们通过介绍“定时任务调度”场景来介绍分布锁的常见方案。

二、分布式锁常见方案

不知道大家第一次写定时任务的时候,会不会犯个错误。就是定时任务被部署在不同的服务器上的服务一起执行(被多分片服务执行)。如果是查询类型的任务可能影响不大,但是如果是修改类型任务(涉及到文件、数据库等)时,可能就有引起较为严重的问题。

1、redis/zookeeper

一般推荐使用中间件提供的分布式锁,来保证只有一个服务去执行定时任务,例如使用redis,在执行任务前先去获取设置一个key,谁设置成功让谁继续执行下去,失败则直接结束当前任务。

set key value nx ex duration   
# nx 表示只有在键不存在时才设置成功,ex 表示要给键的设置过期时间

但是可能会有一些问题,如果定时任务准备启动时,redis出现故障如何处理?简单方案是将key的过期时间设置为半天或者一天,然后定时任务设置@Scheduled(cron = "0 0 1,2,3 * * ?")成当天执行多次,如果前面有执行成功的话,因为key的存在就不会再执行了。如果redis宕机一夜都没处理好,那确实尴尬了。

同理用redis设置分布式锁,也能用zookeeper设置,客户端通过zookeeper在指定路径下创建一个临时顺序节点,并获取所有子节点列表。如果客户端创建的节点是当前节点列表中序号最小的节点,则代表该客户端获取到了锁。

//<dependency>
//    <groupId>org.apache.curator</groupId>
//    <artifactId>curator-recipes</artifactId>
//    <version>5.3.0</version>
//</dependency>

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class ZookeeperLockExample {
    private static final String ZK_CONNECTION_STRING = "127.0.0.1:2181";
    private static final String LOCK_PATH = "/mylock";

    public static void main(String[] args) throws Exception {
        CuratorFramework client = CuratorFrameworkFactory.newClient(ZK_CONNECTION_STRING, new ExponentialBackoffRetry(1000, 3));
        client.start();
        InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);
        try {
            // 尝试获取锁
            lock.acquire();
            // 任务处理
        } finally {
            // 释放锁
            lock.release();
        }
        client.close();
    }
}

2、绑定机器

也有一些其他的特殊需求,可能需要在特定的机器上面运行对应的定时任务(仅授权某个机器有权限访问相关服务等)。此时就需要硬编码(后续可通过Apollo或Nacos等配置中心控制),定时任务增加判断读取服务器特殊标识符(ip地址等),符合条件才可以执行。保证服务器上运行的服务实例(分片)只有一个,保证在定时任务执行时对应的机器不会出现故障。

3、数据库锁

不使用rediszk的分布式锁,而是用数据库的行锁帮忙。一般不推荐使用这个方案,除非是没有外部中间件可用的话才考虑。建一张表,只有一行数据。switch 字段表示开关,决定是否开启定时任务,start_time 字段表示最近一次定时任务开始执行的时间,status 字段表示是否加锁防止其他服务器再执行,表结构如下。

table_name: schedule_status

id switch start_time status
1  1  2020/10/11 21:08:00  1
假设有两个事务,分别是A和B

事务A:
开始事务;
select * from schedule_status where id=1 for update;
update schedule_status set status=0 where id=1;
提交事务;

事务B:
开始事务;
select * from schedule_status where id=1 for update;
update schedule_status set status=0 where id=1;
提交事务;

如果事务A先执行了 select,即使事务B也 select 到了 status=1,但是事务A通过for update把该行数据锁住(排他锁),事务B只能查不能改。等事务A把status改为0,事务B则不能执行本次定时任务,等待下次时间点再争夺。至于‍start_time字段是防止某台服务器在执行完定时任务之后在恢复status为1时出现了故障,所以需要每次执行定时任务时,遇到status为0之后,再通过当前时间-start_time是否超时30min左右,如果超时我们可以认为上次执行定时任务的服务器宕机之类的,此时我们接着返回true让先发现这个故障的服务器先执行,并且同时更新一下对应start_time。这样即使节点挂掉,也不影响下一次定时任务的执行。

三、小结

本篇文件通过“定时任务调度”的业务场景来介绍分布式锁的常用方案,希望大家在开发过程中遇到分布式场景时能做好应对策略。

北白犀

北部白犀牛🦏
    北部白犀牛和南部白犀牛同属白犀亚种,与非洲南部的白犀在基因上存在较大差异。2018年3月19日,世界上最后一
    头雄性北方白犀牛“苏丹”在肯尼亚去世,终年45岁。尽管犀牛角的交易在全球范围内被禁止,但在黑市内仍然热火朝
    天,在也门就有专门的犀牛角市场,在那里以犀牛角制成手柄的匕首是众多买家和卖家关注的焦点,是身份的象征。
    利益熏心的偷猎者每年都大量猎杀这些珍贵的白犀,而面对偷猎猖獗,非洲国家由于落后的经济技术无暇应对,这些
    问题已经导致北白犀成为即将灭绝在现代文明面前的大型动物。

老博客原文链接