资讯专栏INFORMATION COLUMN

分布式锁(zookeeper)与接口幂等性实现

zhigoo / 413人阅读

摘要:分布式系统错综复杂,今天,我们着重对分布式系统的互斥性与幂等性进行分析与解决。阻塞锁与自旋锁。公平锁与非公平锁。实现今天重点讲解使用实现分布式锁。个人感觉是最适合实现分布式锁。如以上流程,接口无法幂等,可能导致重复扣款。

背景

随着数据量的增大,用户的增多,系统的并发访问越来越大,传统的单机已经满足不了需求,分布式系统成为一种必然的趋势。分布式系统错综复杂,今天,我们着重对分布式系统的互斥性与幂等性进行分析与解决。

互斥性

互斥性问题也就是共享资源的抢占问题。如何解决呢?也就是锁,保证对共享资源的串行化访问。互斥性要如何实现?。在java中,最常用的是synchronized和lock这两种内置的锁,但这只适用于单进程中的多线程。对于在同一操作系统下的多个进程间,常见的锁实现有pv信号量等。然而,当问题扩展到多台机器的多个操作系统时,也就是分布式锁,情况就复杂多了。

锁要存在哪里。必须提供一个所有主机都能访问到的存储空间

加锁的进程在挂掉之后,如何确保锁被解开,释放资源。可以通过超时机制或者定时检测心跳来实现

不同进程间如何获取相同的唯一标识来竞争锁。可以利用要保护的资源生成一个唯一的id

获取锁操作的原子性。必须保证读取锁状态、加锁两步的原子性

锁的可重入性。某个线程试图再次获取由自己持有的锁,这个操作会百分百成功,这就是可重入性。如果不能保证可重入性,就会有死锁的可能。

阻塞锁与自旋锁。当获取不到锁时,阻塞锁就是线程阻塞自身,等待唤醒,自旋锁就是不断的尝试重新获取锁。

公平锁与非公平锁。公平锁保证按照请求的顺序获取锁,非公平锁就是可以插队。公平锁一般要维持一个队列来实现,所以非公平锁的性能会更好一点。

避免惊群效应。如果分布式锁是阻塞锁,当锁的占有者释放锁时,要避免同时唤醒多个阻塞的线程,产生惊群效应。

zookeeper实现

今天重点讲解使用zookeeper实现分布式锁。个人感觉zookeeper是最适合实现分布式锁。它的几个特性:

顺序节点:可以避免惊群效应

临时节点:避免机器宕机倒是锁无法释放

watch机制:可以及时唤醒等待的线程

zk实现分布式锁的流程如下

我这里用zk实现了一个可重入的、阻塞的、公平的分布式锁,代码如下:

package locks;

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import utils.ZkUtils;
import watcher.PredecessorNodeWatcher;
import watcher.SessionWatcher;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* Created by huangwt on 2018/3/21.
*/
@Slf4j
public class ReentrantZKLock {

    private final static String BASE_NODE = "/baseNode";
    private final static String CHILDREN_NODE = "/node_";

    private final Lock localLock;
    private final Condition condition;

    //用于重入检测
    private static ThreadLocal threadLocal = new ThreadLocal();

    private ZooKeeper zooKeeper = null;

    private String node = null;

    ReentrantZKLock(String addr, int timeout) {
        try {
            zooKeeper = new ZooKeeper(addr, timeout, new SessionWatcher());
            localLock = new ReentrantLock();
            condition = localLock.newCondition();
        } catch (IOException e) {
            log.error("get zookeeper failed", e);
            throw new RuntimeException(e);
        }
    }

    public void lock() {
        //重入检测
        if (checkReentrant()) {
            return;
        }
        try {
            node = zooKeeper.create(BASE_NODE + CHILDREN_NODE, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            while (true) {
                localLock.lock();
                try {
                    List childrenNodes = zooKeeper.getChildren(BASE_NODE, false);
                    ZkUtils.childNodeSort(childrenNodes);
                    //当前节点的索引
                    int myNodeIndex = childrenNodes.indexOf(node);
                    //当前节点的前一个节点
                    int beforeNodeIndex = myNodeIndex - 1;
                    Stat stat = null;
                    while (beforeNodeIndex >= 0) {
                        stat = zooKeeper.exists(childrenNodes.get(beforeNodeIndex), new PredecessorNodeWatcher(condition));
                        if (stat != null) {
                            break;
                        }
                    }

                    if (stat != null) {  //前序节点存在,等待前序节点被删除,释放锁
                        condition.await();
                    } else { // 获取到锁
                        threadLocal.set(new AtomicInteger(1));
                        return;
                    }
                } finally {
                    localLock.unlock();
                }
            }
        } catch (Exception e) {
            log.error("lock failed", e);
            throw new RuntimeException(e);
        }

    }

    public void unlock() {
        AtomicInteger times = threadLocal.get();
        if (times == null) {
            return;
        }
        if (times.decrementAndGet() == 0) {
            threadLocal.remove();
            try {
                zooKeeper.delete(node, -1);
            } catch (Exception e) {
                log.error("unlock faild", e);
                throw new RuntimeException(e);
            }
        }

    }

    private boolean checkReentrant() {
        AtomicInteger times = threadLocal.get();
        if (times != null) {
            times.incrementAndGet();
            return true;
        }

        return false;
    }
}
package utils;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
* Created by huangwt on 2018/3/24.
*/
public class ZkUtils {

    /**
    * 对子节点排序
    *
    * @param node
    */
    public static void childNodeSort(List node) {
        Collections.sort(node, new ChildNodeCompare());
    }

    private static class ChildNodeCompare implements Comparator {

        public int compare(String childNode1, String childNode2) {
            return childNode1.compareTo(childNode2);
        }
    }

}
package watcher;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;

import java.util.concurrent.locks.Condition;

/**
* Created by huangwt on 2018/3/24.
*/
public class PredecessorNodeWatcher implements Watcher {
    private Condition condition = null;

    public PredecessorNodeWatcher(Condition condition) {
        this.condition = condition;
    }

    public void process(WatchedEvent event) {
        //前序节点被删除,锁被释放,唤醒当前等待线程
        if(event.getType() == Event.EventType.NodeDeleted){
            condition.signal();
        }
    }
}
package watcher;

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;

/**
* Created by huangwt on 2018/3/24.
*/
@Slf4j
public class SessionWatcher implements Watcher {
    public void process(WatchedEvent event) {
        if (event.getState() == Event.KeeperState.SyncConnected) {
            log.info("get zookeeper success");
        }
    }
}

主要是使用了ThreadLocal实现了锁的可重入性,使用watch机制实现了阻塞锁,使用临时节点实现的公平锁。
这段代码只是一个demo供大家参考,还有很多问题没解决。比如当zookper挂掉的时候,阻塞的线程就无法被唤醒,这时候就需要监听zk的心跳。

幂等性

幂等性是系统接口对外的一种承诺,数学表达为:f(f(x)) = f(x)。
幂等性指的是,使用相同参数对同一资源重复调用某个接口的结果与调用一次的结果相同。

为什么需要幂等性?

假设现在有一个方法 :Boolean withdraw(account_id, amount) ,作用是从account_id对应的账户中扣除amount数额的钱,如果扣除成功则返回true,账户余额减少amount; 如果扣除失败则返回false,账户余额不变。
如以上流程,接口无法幂等,可能导致重复扣款。

解决

请求获取ticketId

请求扣款,传入ticketId

根据ticketId查询此次操作是否存在,如果存在则表示该操作已经执行过,直接返回结果;如果不存在,扣款,保存结果

返回结果到客户端

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69250.html

相关文章

  • 等性的作用及实现

    摘要:保证幂等性的方法建立唯一索引,防止新增脏数据这个可以限制重复插入数据,当重复时,数据库会抛异常,保证不会出现脏数据。它用来解决分布式系统的幂等性,常用的实现方案是和等工具。 幂等性 幂等这个词原自数学,某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。第一次请求的时候对资源产生了副作用,但...

    Java3y 评论0 收藏0
  • 【最全】Java 进阶面试总结

    摘要:这里有一份面试题相关总结,涉及高并发分布式高可用相关知识点,在此分享给大家,希望大家能拿到一份理想的知识点会陆续更新在上,觉得还算凑和的话可以关注一下噢高并发架构消息队列为什么使用消息队列消息队列有什么优点和缺点都有什么优点和缺点如何保证消 这里有一份面试题相关总结,涉及高并发、分布式、高可用相关知识点,在此分享给大家,希望大家能拿到一份理想的 Offer! 知识点会陆续更新在 Git...

    nifhlheimr 评论0 收藏0
  • 五万字15张导图Java自学路线,小白零基础入门,程序员进阶,收藏这篇就够了

    摘要:本文收录于技术专家修炼文中配套资料合集路线导图高清源文件点击跳转到文末点击底部卡片回复资料领取哈喽,大家好,我是一条最近粉丝问我有没有自学路线,有了方向才能按图索骥,事半功倍。 ...

    suosuopuo 评论0 收藏0
  • 微服务化之无状态化容器化

    摘要:然而在微服务化之前,建议先进行容器化,在容器化之前,建议先无状态化,当整个流程容器化了,以后的微服务拆分才会水到渠成。 此文已由作者刘超授权网易云社区发布。 欢迎访问网易云社区,了解更多网易技术产品运营经验。 一、为什么要做无状态化和容器化 很多应用拆分成微服务,是为了承载高并发,往往一个进程扛不住这么大的量,因而需要拆分成多组进程,每组进程承载特定的工作,根据并发的压力用多个副本公共...

    seanlook 评论0 收藏0
  • 【推荐】最新200篇:技术文章整理

    摘要:作为面试官,我是如何甄别应聘者的包装程度语言和等其他语言的对比分析和主从复制的原理详解和持久化的原理是什么面试中经常被问到的持久化与恢复实现故障恢复自动化详解哨兵技术查漏补缺最易错过的技术要点大扫盲意外宕机不难解决,但你真的懂数据恢复吗每秒 作为面试官,我是如何甄别应聘者的包装程度Go语言和Java、python等其他语言的对比分析 Redis和MySQL Redis:主从复制的原理详...

    BicycleWarrior 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<