分布式锁的应用场景与分布式锁实现(三):基于Zookeeper实现分布式锁

news2025/4/27 4:48:48

分布式锁的应用场景与分布式锁实现(二):基于Redis实现分布式锁

基于Zookeeper实现分布式锁

​ 实现分布式锁目前有三种流行方案,分别为基于数据库、Redis、Zookeeper的方案。这里主要介绍基于zk怎么实现分布式锁。在实现分布式锁之前,先了解Zookeeper的相关知识。

image-20220622191952608

Zookeeper相关知识

安装启动

​ 安装:将zk安装包上传到/opt目录下,并切换到/opt目录,执行以下指令

# 解压
tar -zxvf zookeeper-3.7.0-bin.tar.gz
# 重命名
mv apache-zookeeper-3.7.0-bin/ zookeeper
# 打开zookeeper根目录
cd /opt/zookeeper
# 创建一个数据目录,备用
mkdir data
# 打开zk的配置目录
cd /opt/zookeeper/conf
# copy配置文件,zk启动时会加载zoo.cfg文件
cp zoo_sample.cfg zoo.cfg
# 编辑配置文件
vim zoo.cfg
# 修改dataDir参数为之前创建的数据目录:/opt/zookeeper/data
# 切换到bin目录
cd /opt/zookeeper/bin
# 启动 
./zkServer.sh start
./zkServer.sh status # 查看启动状态
./zkServer.sh stop # 停止
./zkServer.sh restart # 重启
./zkCli.sh # 查看zk客户端

image-20230530203531561

​ 如下,说明启动成功:

image-20230530203620352

​ 相关指令:

  • ls / :查看某个节点下的子节点
  • get /zookeeper :查看zookeeper节点信息
  • create /aa “test” :创建持久化节点
  • delete /aa :删除节点
  • set /aa “test1” :设置节点内容

相关概念

​ Zookeeper提供一个多层级的节点命名空间(节点成为znode),每个节点都用一个以斜杠(/)分割的路径表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。并且每个节点都是唯一的。

​ znode节点有四种类型:

  • PERSISTENT:永久节点。客户端与Zookeeper断开连接后,该节点依旧存在
  • EPHEMERAL:临时节点。客户端与Zookeeper断开连接后,该节点被删除
  • PERSISTENT_SEQUENTIAL:永久节点、序列化。客户端与Zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
  • EPHEMERAL_SEQUENTIAL:临时节点、序列化。客户端与Zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
# 在bin目录运行: ./zkCli.sh进入控制台

[zk: localhost:2181(CONNECTED) 0] create /aa test  # 创建持久化节点
Created /aa
[zk: localhost:2181(CONNECTED) 1] create -s /bb test  # 创建持久序列化节点
Created /bb0000000001
[zk: localhost:2181(CONNECTED) 2] create -e /cc test  # 创建临时节点
Created /cc
[zk: localhost:2181(CONNECTED) 3] create -e -s /dd test  # 创建临时序列化节点
Created /dd0000000003
[zk: localhost:2181(CONNECTED) 4] ls /   # 查看某个节点下的子节点
[aa, bb0000000001, cc, dd0000000003, zookeeper]
[zk: localhost:2181(CONNECTED) 5] stat /  # 查看某个节点的状态
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x5
cversion = 3
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 5
[zk: localhost:2181(CONNECTED) 6] get /aa  # 查看某个节点的内容
test
[zk: localhost:2181(CONNECTED) 11] delete /aa  # 删除某个节点
[zk: localhost:2181(CONNECTED) 7] ls /  # 再次查看
[bb0000000001, cc, dd0000000003, zookeeper]

​ 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,Zookeeper会通知客户端。节点监听是一次性的。当前Zookeeper针对节点的监听有如下四种事件:

  • 节点创建: stat -w /xx
    • 当/xx节点创建时: NodeCreated
  • 节点删除:stat -w /xx
    • 当/xx节点删除是:NodeDeleted
  • 节点数据修改:get -w /xx
    • 当/xx节点数据发生变化时:NodeDataChanged
  • 子节点变更:ls -w /xx
    • 当/xx节点的子节点创建或者删除是:NodeChildChanged

Java客户端

​ Zookeeper的Java客户端有:原生客户端、ZkClient、Curator框架(类似于Redisson,有很多功能性封装)。

  • 引入原生客户端依赖
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.7.0</version>
</dependency>
  • 常用API及其方法
package tech.msop.distributed.lock.zk;

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * Zookeeper测试类
 */
@Slf4j
public class ZkTest {

    public static void main(String[] args) throws InterruptedException {
        ZooKeeper zooKeeper = null;
        CountDownLatch countDownLatch = new CountDownLatch(1);
        try {
            zooKeeper = new ZooKeeper("192.168.56.99:2181", 30000, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    Event.KeeperState state = watchedEvent.getState();
                    if (Event.KeeperState.SyncConnected.equals(state)
                            && Event.EventType.None.equals(watchedEvent.getType())
                    ) {
                        log.info("获取链接");
                        countDownLatch.countDown();
                    } else if (Event.KeeperState.Closed.equals(state)) {
                        log.info("关闭连接");
                    } else {
                        log.info("节点监测");
                    }

                }
            });
            countDownLatch.await();
            log.info("操作数据");
            // 节点新增:永久 临时 永久序列化 临时序列化
            // 创建一个节点,1-节点路径 2-节点内容 3-节点的访问权限 4-节点类型
            // OPEN_ACL_UNSAFE:任何人可以操作该节点
            // CREATOR_ALL_ACL:创建者拥有所有访问权限
            // READ_ACL_UNSAFE: 任何人都可以读取该节点
            // 创建永久节点
//            zooKeeper.create("/zookeeper/test","hello Zookeeper".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            // 创建临时节点
//            zooKeeper.create("/zookeeper/test1","hello Zookeeper".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            // 创建永久序列化节点
            zooKeeper.create("/zookeeper/test2", "hello Zookeeper".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
            // 创建临时序列化节点
//            zooKeeper.create("/zookeeper/test3","hello Zookeeper".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            log.info("节点查询");
            // 查询  判断节点是否存在 stat  节点监听,watch:true
            Stat stat = zooKeeper.exists("/zookeeper", true);
            if (stat != null) {
                log.info("当前节点存在");
            } else {
                log.warn("当前节点不存在");
            }
            // 获取当前节点中的数据内容 get
            byte[] data = zooKeeper.getData("/zookeeper", false, stat);
            log.info("当前节点的内容:{}", new String(data));
            // 获取当前节点的子节点 ls  节点监听,watch:true
            List<String> children = zooKeeper.getChildren("/zookeeper", new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    log.info("节点的子节点发生变化");
                }
            });
            log.info("当前节点的子节点:{}", children);

            // 更新:版本号必须和当前节点的版本号一致,否则更新失败。也可以指定为-1,表示不关心版本号
            zooKeeper.setData("/zookeeper", "hello zookeeper node".getBytes(), stat.getVersion());
            // 删除节点
//            zooKeeper.delete("/zookeeper/test1",-1);

            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            throw new RuntimeException(e);
        } finally {
            if (zooKeeper != null) {
                zooKeeper.close();
            }
        }

    }
}

思路分析

​ 分布式锁的步骤:

  • 1、获取锁:create一个节点
  • 2、删除锁:delete一个节点
  • 3、重试:没有获取到锁的请求重试

​ 参照Redis分布式锁的特点:

  • 1、互斥 排他
  • 2、防死锁:
    • 1)可自动释放锁(临时节点):获得锁之后客户端所在及其宕机了,客户端没有主动删除子节点;如果创建的是永久节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一段时间Zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。
    • 2)可重入锁:借助ThreadLocal
  • 3、防误删:宕机自动释放临时节点,不需要设置过期时间,也就不存在误删问题。
  • 4、加锁/解锁要具备原子性
  • 5、单点问题:使用Zookeeper可以有效的解决单点问题,ZK一般是集群部署的。
  • 6、集群问题:Zookeeper集群是强一致性的,只要集群中有半数以上的机器存活,就可以对外提供服务。

基本实现

​ 实现思路:

  • 1、多个请求同时添加一个相同的临时节点,只有一个可以添加成功。添加成功的获取到锁
  • 2、执行业务逻辑
  • 3、完成业务流程后,删除节点释放锁。

​ 由于Zookeeper获取连接是一个耗时过程,这里可以在项目启动时,初始化连接,并且只初始化一次。借助Spring特性,实现如下代码:

​ 新增Zookeeper相关常量:

package tech.msop.distributed.lock.constants;

public interface StockConstant {
    /**
     * 默认库存数
     */
    Integer DEFAULT_STOCK_COUNT = 5000;
    /**
     * Redis 锁默认过期时间
     */
    long DEFAULT_REDIS_EXPIRE = 30;
    /**
     * Zookeeper分布式锁的连接地址
     */
    String ZOOKEEPER_LOCK_CONNECT_URL = "192.168.56.99:2181";
    /**
     * Zookeeper分布式锁的节点根路径
     */
    String ZOOKEEPER_LOCK_ROOT_PATH = "/distributed";
}

​ 新增ZkClient客户端:

package tech.msop.distributed.lock.zk;

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.CountDownLatch;

import static tech.msop.distributed.lock.constants.StockConstant.ZOOKEEPER_LOCK_CONNECT_URL;
import static tech.msop.distributed.lock.constants.StockConstant.ZOOKEEPER_LOCK_ROOT_PATH;

@Component
@Slf4j
public class ZkClient {

    private ZooKeeper zooKeeper = null;

    @PostConstruct
    public void init() {
        try {
            zooKeeper = new ZooKeeper(ZOOKEEPER_LOCK_CONNECT_URL, 30000, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    log.info("获取链接成功");
                }
            });
            // 创建分布式锁根节点
            if (this.zooKeeper.exists(ZOOKEEPER_LOCK_ROOT_PATH, false) == null) {
                this.zooKeeper.create(ZOOKEEPER_LOCK_ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 在Spring容器销毁时关闭连接
     */
    @PreDestroy
    public void destory() {
        // 释放zk的链接
        try {
            if (zooKeeper != null) {
                zooKeeper.close();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取ZK分布式锁
     *
     * @param lockName 锁名称
     * @return ZK分布式锁
     */
    public DistributedZkLock getLock(String lockName) {
        return new DistributedZkLock(zooKeeper, lockName);
    }
}

​ 实现Zookeeper分布式锁:

package tech.msop.distributed.lock.zk;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

import static tech.msop.distributed.lock.constants.StockConstant.ZOOKEEPER_LOCK_ROOT_PATH;

public class DistributedZkLock implements Lock {

    private ZooKeeper zooKeeper;
    private String path;

    public DistributedZkLock(ZooKeeper zooKeeper, String lockName) {
        this.zooKeeper = zooKeeper;
        this.path  = ZOOKEEPER_LOCK_ROOT_PATH + "/" + lockName;
    }

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        // 创建znode节点的过程:为了防止ZK客户端程序获取到锁之后,服务器宕机带来的死锁问题,这里创建的是临时节点
        try {
            this.zooKeeper.create(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        } catch (Exception e) {
            e.printStackTrace();
            try {
                Thread.sleep(80);
                this.tryLock();
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        // 删除zonde接地那的过程
        try {
            this.zooKeeper.delete(path, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

    @NotNull
    @Override
    public Condition newCondition() {
        return null;
    }
}

​ 改造服务,使用ZK分布式锁:

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.lock.DistributedLockClient;
import tech.msop.distributed.lock.lock.DistributedRedisLock;
import tech.msop.distributed.lock.zk.DistributedZkLock;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;
import tech.msop.distributed.lock.zk.ZkClient;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 库存服务实现类 <br/>
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private DistributedLockClient distributedLockClient;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ZkClient zkClient;

    /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        DistributedZkLock lock = zkClient.getLock("lock");
        lock.lock();
        try {
            // 1. 查询库存信息
            String stock = redisTemplate.opsForValue().get("stock").toString();
            // 2. 判断库存是否充足
            if (stock != null && stock.length() != 0) {
                Integer st = Integer.valueOf(stock);
                if (st > 0) {
                    // 3.扣减库存
                    redisTemplate.opsForValue().set("stock", String.valueOf(--st));
                }
            }
        } finally {
            lock.unlock();
        }
    }
}

​ 使用Jmeter进行压力测试并查询库存余量:0(注意:所有测试之前都要重置库存量)

image-20230531160135318

​ 基本实现存在的问题:

  • 性能一般(比MySQL分布式锁略好)
  • 不可重入

优化:性能优化

​ 基本实现中由于无限自旋影响性能:

image-20230531160254281

​ 试想:每个请求要想正常的执行完成,最终都是要创建节点,如果能够避免争抢必然可以提高性能。

​ 这里借助于ZK的临时序列化节点,实现分布式锁:

1607048783043

监听实现阻塞锁

​ 在设置事件监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,序号为1的客户端监听序号为0的子节点删除消息,序号为2的监听序号为1的子节点删除消息。

​ 所以调整后的分布式锁算法流程如下:

  • 客户端连接Zookeeper,并在/distributed下创建临时的且有序的子节点,第一个客户端对应的子节点为/distributed/lock-000000000,第二个为/distributed/lock-000000001,以此类推。
  • 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获取锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
  • 执行业务代码;
  • 完成业务流程后,删除对应的子节点释放锁。

​ 改造分布式锁:

package tech.msop.distributed.lock.zk;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;

import static tech.msop.distributed.lock.constants.StockConstant.ZOOKEEPER_LOCK_ROOT_PATH;

@Slf4j
public class DistributedZkLock implements Lock {

    private ZooKeeper zooKeeper;
    private String lockName;

    private String currentNodePath;

    public DistributedZkLock(ZooKeeper zooKeeper, String lockName) {
        this.zooKeeper = zooKeeper;
        this.lockName = lockName;
    }

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        // 创建znode节点的过程:为了防止ZK客户端程序获取到锁之后,服务器宕机带来的死锁问题,这里创建的是临时节点
        try {
            currentNodePath = this.zooKeeper.create(ZOOKEEPER_LOCK_ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            // 获取前置节点,如果前置节点为空,则获取锁成功,否则监听前置节点
            String preNode = this.getPreNode();
            if (preNode != null) {
                // 利用闭锁思想,实现阻塞功能
                CountDownLatch countDownLatch = new CountDownLatch(1);
                // 因为获取前置节点这个操作,不具备原子性,再次判断ZK中的前置节点是否存在
                if (this.zooKeeper.exists(ZOOKEEPER_LOCK_ROOT_PATH + "/" + preNode, new Watcher() {

                    @Override
                    public void process(WatchedEvent watchedEvent) {
                        countDownLatch.countDown();
                    }
                }) == null) {
                    return true;
                }
                countDownLatch.await();
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            // 重试获取锁
//            try {
//                Thread.sleep(200);
//                this.tryLock();
//            } catch (InterruptedException exception) {
//                exception.printStackTrace();
//            }
        }
        return false;
    }

    /**
     * 获取前置节点
     *
     * @return 前置节点路径
     */
    private String getPreNode() {
        // 获取根节点下的所有节点
        try {
            List<String> children = this.zooKeeper.getChildren(ZOOKEEPER_LOCK_ROOT_PATH, false);
            if (CollectionUtils.isEmpty(children)) {
                throw new IllegalMonitorStateException("非法操作");
            }
            // 获取和当前节点同一资源的锁
            List<String> nodes = children.stream().filter(node -> StringUtils.startsWithIgnoreCase(node, lockName + "-")).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(nodes)) {
                throw new IllegalMonitorStateException("非法操作");
            }
            // 排序
            Collections.sort(nodes);
            // 获取当前节点的下标
            // 获取当前接地那的名称
            // 获取当前节点
            String currentNode = StringUtils.substringAfterLast(currentNodePath, "/");
            int index = Collections.binarySearch(nodes, currentNodePath);
            if (index < 0) {
                throw new IllegalMonitorStateException("非法操作");
            } else if (index > 0) {
                return nodes.get(index - 1);// 返回前置节点
            }
            return null;
            // 如果当前节点就是第一个节点,返回null
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalMonitorStateException("非法操作");
        }
    }

    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        // 删除zonde节点的过程
        try {
            this.zooKeeper.delete(currentNodePath, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

    @NotNull
    @Override
    public Condition newCondition() {
        return null;
    }
}

优化:可重入锁

有两种实现方式:

  • 在节点的内容中记录服务器、线程以及重入信息
  • ThreadLocal:线程的局部变量,线程私有

以ThreadLocal为例

package tech.msop.distributed.lock.zk;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;

import static tech.msop.distributed.lock.constants.StockConstant.ZOOKEEPER_LOCK_ROOT_PATH;

@Slf4j
public class DistributedZkLock implements Lock {

    private ZooKeeper zooKeeper;
    private String lockName;

    private String currentNodePath;

    private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();

    public DistributedZkLock(ZooKeeper zooKeeper, String lockName) {
        this.zooKeeper = zooKeeper;
        this.lockName = lockName;
    }

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        // 判断ThreadLocal中是否已经有锁,有锁直接重入(+1)
        Integer flag = THREAD_LOCAL.get();
        if (flag != null && flag > 0){
            THREAD_LOCAL.set(flag + 1);
            return true;
        }

        // 创建znode节点的过程:为了防止ZK客户端程序获取到锁之后,服务器宕机带来的死锁问题,这里创建的是临时节点
        try {
            currentNodePath = this.zooKeeper.create(ZOOKEEPER_LOCK_ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            // 获取前置节点,如果前置节点为空,则获取锁成功,否则监听前置节点
            String preNode = this.getPreNode();
            if (preNode != null) {
                // 利用闭锁思想,实现阻塞功能
                CountDownLatch countDownLatch = new CountDownLatch(1);
                // 因为获取前置节点这个操作,不具备原子性,再次判断ZK中的前置节点是否存在
                if (this.zooKeeper.exists(ZOOKEEPER_LOCK_ROOT_PATH + "/" + preNode, new Watcher() {

                    @Override
                    public void process(WatchedEvent watchedEvent) {
                        countDownLatch.countDown();
                    }
                }) == null) {
                    THREAD_LOCAL.set(1);
                    return true;
                }
                countDownLatch.await();
            }
            THREAD_LOCAL.set(1);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            // 重试获取锁
//            try {
//                Thread.sleep(200);
//                this.tryLock();
//            } catch (InterruptedException exception) {
//                exception.printStackTrace();
//            }
        }
        return false;
    }

    /**
     * 获取前置节点
     *
     * @return 前置节点路径
     */
    private String getPreNode() {
        // 获取根节点下的所有节点
        try {
            List<String> children = this.zooKeeper.getChildren(ZOOKEEPER_LOCK_ROOT_PATH, false);
            if (CollectionUtils.isEmpty(children)) {
                throw new IllegalMonitorStateException("非法操作");
            }
            // 获取和当前节点同一资源的锁
            List<String> nodes = children.stream().filter(node -> StringUtils.startsWithIgnoreCase(node, lockName + "-")).collect(Collectors.toList());
            if (CollectionUtils.isEmpty(nodes)) {
                throw new IllegalMonitorStateException("非法操作");
            }
            // 排序
            Collections.sort(nodes);
            // 获取当前节点的下标
            // 获取当前接地那的名称
            // 获取当前节点
            String currentNode = StringUtils.substringAfterLast(currentNodePath, "/");
            int index = Collections.binarySearch(nodes, currentNodePath);
            if (index < 0) {
                throw new IllegalMonitorStateException("非法操作");
            } else if (index > 0) {
                return nodes.get(index - 1);// 返回前置节点
            }
            return null;
            // 如果当前节点就是第一个节点,返回null
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalMonitorStateException("非法操作");
        }
    }

    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        try {
            // 重入锁 - 1
            THREAD_LOCAL.set(THREAD_LOCAL.get() - 1);
            if (THREAD_LOCAL.get() == 0){
                // 删除zonde节点的过程
                this.zooKeeper.delete(currentNodePath, -1);
            }
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

    @NotNull
    @Override
    public Condition newCondition() {
        return null;
    }
}

ZK分布式锁小结

​ 参照Redis分布式锁的特点:

  • 互斥 排他:ZK节点的不可重复性,以及序列化节点的有序性
  • 防死锁:
    • 可自动释放锁:临时节点
    • 可重入锁:借助于ThreadLocal
  • 防误删:给每一个请求线程创建一个唯一的序列化节点
  • 加锁/解锁需要具备原子性
    • 创建节点、删除节点、查询及监听具备原子性
  • 自动续期:没有过期时间,不需要自动续期
  • 单点问题:使用ZooKeeper可以有效的解决单点问题,ZK一般是集群部署的
  • 集群问题:Zookeeper集群是强一致性的,只要集群中有半数以上的机器存活,就可以对外提供服务
  • 公平锁:有序性节点

Curator中的分布式锁

​ Curator是netflix公司开源的一套zookeeper客户端,目前是Apache的顶级项目。与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。Curator解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册watcher和NodeExistsException异常等。

​ 通过查看官方文档,可以发现Curator主要解决了三类问题:

  • 封装Zookeeper client与Zookeeper server之间的连接处理
  • 提供了一套Fluent风格的操作API
  • 提供Zookeeper各种应用场景(recipe,例如:分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等)的抽象封装,这些实现都遵循了zk的最佳实践,并考虑了各种极端情况。

​ Curator由一系列的模块构成,对于一般开发中而言,常用的是curator-framework和curator-recipes:

  • curator-framework:提供了常见的ZK相关的底层操作
  • curator-recipes:提供了一些ZK的典型应用场景的参考。比如分布式锁。

​ 引入依赖:需要注意curator传递进来的依赖,需要和实际服务器端使用的版本相符,所以会排除curator的依赖,使用符合服务器版本的zookeeper依赖

<!-- curator -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.3.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.3.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
 <!-- Zookeeper -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.7.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

​ 添加Curator客户端配置

package tech.msop.distributed.lock.config;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.msop.distributed.lock.constants.StockConstant;

/**
 * Curator配置
 */
@Configuration
public class CuratorConfig {
    @Bean
    public CuratorFramework curatorFramework(){
        // 初始化一个重试策略。这里使用的指数补偿策略。初始间隔时间(毫秒) 重试册数
        RetryPolicy retry = new ExponentialBackoffRetry(10000, 3);
        // 初始化curator客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(StockConstant.ZOOKEEPER_LOCK_CONNECT_URL, retry);
        // 手动启动,否则很多方法或者功能不工作的
        client.start();
        return client;
    }
}

可重入锁 InterProcessMutex

​ Reentrant和JDK的ReentrantLock类似,意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。它是由类InterProcessMutex实现。

// 常用构造方法
public InterProcessMutex(CuratorFramework client,String path)
// 获取锁
public void acquire();
// 带超时时间的可重入锁
public boolean acquire(long time,TimeUnit unit);
// 释放锁
public void release();

​ 改造Service测试方法

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.lock.DistributedLockClient;
import tech.msop.distributed.lock.lock.DistributedRedisLock;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;
import tech.msop.distributed.lock.zk.ZkClient;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 库存服务实现类 <br/>
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private DistributedLockClient distributedLockClient;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ZkClient zkClient;
    @Autowired
    private CuratorFramework curatorFramework;

    /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/curator/locks");
        try {
            mutex.acquire();
            // 1. 查询库存信息
            String stock = redisTemplate.opsForValue().get("stock").toString();
            // 2. 判断库存是否充足
            if (stock != null && stock.length() != 0) {
                Integer st = Integer.valueOf(stock);
                if (st > 0) {
                    // 3.扣减库存
                    redisTemplate.opsForValue().set("stock", String.valueOf(--st));
                }
            }
            // 测试可重入性需要同一个InterProcessMutex对象
//            this.testSubLock(mutex);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                mutex.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 测试InterProcessMutex的可重入性
     * @param mutex 分布式锁
     */
    public void testSubLock(InterProcessMutex mutex) {
        try {
            mutex.acquire();
            log.info("测试可重入锁");
            mutex.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

​ 注意:如想重入,则需要使用同一个InterProcessMutex对象

​ 使用Jmeter进行压力测试并查询库存余量:0

image-20230601101637802

不可重入锁 InterProcessSemaphoreMutex

​ 具体先:InterProcessSemaphoreMutex。与InterProcessMutex调用方法类似,区别在于该锁是不可重入的,在同一个线程不可重入。

// 常用构造方法
public InterProcessSemaphoreMutex(CuratorFramework client,String path)
// 获取锁
public void acquire();
// 带超时时间的可重入锁
public boolean acquire(long time,TimeUnit unit);
// 释放锁
public void release();

​ 改造方法:

package tech.msop.distributed.lock.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.lock.DistributedLockClient;
import tech.msop.distributed.lock.lock.DistributedRedisLock;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;
import tech.msop.distributed.lock.zk.ZkClient;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 库存服务实现类 <br/>
 */
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>
        implements IStockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private DistributedLockClient distributedLockClient;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ZkClient zkClient;
    @Autowired
    private CuratorFramework curatorFramework;

    /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        InterProcessSemaphoreMutex mutex = new InterProcessSemaphoreMutex(curatorFramework, "/curator/locks");
        try {
            mutex.acquire();
            // 1. 查询库存信息
            String stock = redisTemplate.opsForValue().get("stock").toString();
            // 2. 判断库存是否充足
            if (stock != null && stock.length() != 0) {
                Integer st = Integer.valueOf(stock);
                if (st > 0) {
                    // 3.扣减库存
                    redisTemplate.opsForValue().set("stock", String.valueOf(--st));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                mutex.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

可重入读写锁 InterProcessReadWriteLock

​ 类似JDK的ReentrantReadWriteLock。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁,但读写不能升级成写锁。主要实现类:InterProcessReadWriteLock:

// 常用构造方法
public ReentrantReadWriteLock(CuratorFramework client,String path)
// 获取读锁对象
InterProcessMutex readLock();
// 获取写锁对象
InterProcessMutex writeLock();

​ 注意:

  • 读读可以并发
  • 读写不可以并发
  • 写写不可以并发
  • 写锁在释放之前会阻塞请求线程,读锁不会
/**
* 测试ZK读锁
*/
@Override
public void testZkReadLock() {
    try {
        InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(curatorFramework, "/curator/rwLock");
        readWriteLock.readLock().acquire(10, TimeUnit.SECONDS);
        log.info("操作数据");
        //            readWriteLock.readLock().release();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
* 测试ZK写锁
*/
@Override
public void testZkWriteLock() {
    try {
        InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(curatorFramework, "/curator/rwLock");
        readWriteLock.writeLock().acquire(10, TimeUnit.SECONDS);
        log.info("操作数据");
        //            readWriteLock.writeLock().release();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

联锁 InterProcessMultiLock(了解)

​ Multi Shared Lock是一个锁的容器。当调用acquire, 所有的锁都会被acquire,如果请求失败,所有的锁都会被release。同样调用release时所有的锁都被release(失败被忽略)。基本上,它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。实现类InterProcessMultiLock:

// 构造函数需要包含的锁的集合,或者一组ZooKeeper的path
public InterProcessMultiLock(List<InterProcessLock> locks);
public InterProcessMultiLock(CuratorFramework client, List<String> paths);

// 获取锁
public void acquire();
public boolean acquire(long time, TimeUnit unit);

// 释放锁
public synchronized void release();

信号量 InterProcessSemaphoreV2

​ 一个计数的信号量类似JDK的Semaphore。JDK中Semaphore维护的一组许可(permits),而Cubator中称之为租约(Lease)。注意,所有的实例必须使用相同的numberOfLease值。调用acquire会返回一个租约对象。客户端必须在finally中close这些租约对象,否则这些租约会丢失掉。但是,如果客户端session由于某种原因比如crash丢掉,那么这些客户端持有的租约会自动close,这样其他客户端可以继续使用这些租约。主要实现类:InterProcessSemaphoreV2

// 构造方法
public InterProcessSemaphoreV2(CuratorFramework client, String path, int maxLeases);

// 注意一次你可以请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞。
// 同时还提供了超时的重载方法
public Lease acquire();
public Collection<Lease> acquire(int qty);
public Lease acquire(long time, TimeUnit unit);
public Collection<Lease> acquire(int qty, long time, TimeUnit unit)

// 租约还可以通过下面的方式返还
public void returnAll(Collection<Lease> leases);
public void returnLease(Lease lease);

​ 测试案例,复制原Redis的信号量,修改为ZK信号量

public void testSemaphore() {
    // 设置资源量,请求的线程数
    InterProcessSemaphoreV2 semaphoreV2 = new InterProcessSemaphoreV2(curatorFramework, "/curator/semaphore", 5);
    try {
        Lease lease = semaphoreV2.acquire();// 获取资源,获取资源成功的线程可以继续处理业务操作,否则会被阻塞
        redisTemplate.opsForList().rightPush("log", "获取到了资源,开始处理业务逻辑" + Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(new Random().nextInt(10));
        redisTemplate.opsForList().rightPush("log", "处理完业务逻辑,释放资源" + Thread.currentThread().getName());
        semaphoreV2.returnLease(lease);// 手动释放资源,后续请求线程可以获取该资源
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

栅栏 barrier

DistributedBarrier构造函数中barrierPath参数用来确定一个栅栏,只要barrierPath参数相同(路径相同)就是同一个栅栏。通常情况下栅栏的使用如下:

  • 1、主client设置一个栅栏
  • 2、其他客户端就会调用waitOnBarrier()等待栅栏移除,程序处理线程阻塞
  • 3、主client移除栅栏,其他客户端的处理程序就会同时继续运行。

​ DistributedBarrier类的主要方法如下:

setBarrier() - 设置栅栏
waitOnBarrier() - 等待栅栏移除
removeBarrier() - 移除栅栏

​ DistributedDoubleBarrier双栅栏,允许客户端在计算的开始和结束时同步。当足够的进程加入到双栅栏时,进程开始计算,当计算完成时,离开栅栏。DistributedDoubleBarrier实现了双栅栏的功能。构造函数如下:

// client - the client
// barrierPath - path to use
// memberQty - the number of members in the barrier
public DistributedDoubleBarrier(CuratorFramework client, String barrierPath, int memberQty);

enter()enter(long maxWait, TimeUnit unit) - 等待同时进入栅栏
leave()leave(long maxWait, TimeUnit unit) - 等待同时离开栅栏

​ memberQty是成员数量,当enter方法被调用时,成员被阻塞,直到所有的成员都调用了enter。当leave方法被调用时,它也阻塞调用线程,直到所有的成员都调用了leave。

​ 注意:参数memberQty的值只是一个阈值,而不是一个限制值。当等待栅栏的数量大于或等于这个值栅栏就会打开!

​ 与栅栏(DistributedBarrier)一样,双栅栏的barrierPath参数也是用来确定是否是同一个栅栏的,双栅栏的使用情况如下:

  • 1、从多个客户端在同一个路径上创建双栅栏(DistributedDoubleBarrier),然后调用enter()方法,等待栅栏数量达到memberQty时就可以进入栅栏。

  • 2、栅栏数量达到memberQty,多个客户端同时停止阻塞继续运行,直到执行leave()方法,等待memberQty个数量的栅栏同时阻塞到leave()方法中。

  • 3、memberQty个数量的栅栏同时阻塞到leave()方法中,多个客户端的leave()方法停止阻塞,继续运行。

共享计数器

​ 利用Zookeeper可以实现一个集群共享的计数器。只要使用相同的path就可以得到最新的计数器值,这是由Zookeeper的一致性保证的。Curator有两个计数器,一个使用int来计数,一个用long来计数。

SharedCount

​ 共享计数器SharedCount相关方法如下:

// 构造方法
public SharedCount(CuratorFramework client, String path, int seedValue);
// 获取共享计数的值
public int getCount();
// 设置共享计数的值
public void setCount(int newCount) throws Exception;
// 当版本号没有变化时,才会更新共享变量的值
public boolean  trySetCount(VersionedValue<Integer> previous, int newCount);
// 通过监听器监听共享计数的变化
public void addListener(SharedCountListener listener);
public void addListener(final SharedCountListener listener, Executor executor);
// 共享计数在使用之前必须开启
public void start() throws Exception;
// 关闭共享计数
public void close() throws IOException;

​ 使用案例:

public void testShareCount() {
    try {
        // 第三个参数是共享计数器的初始值
        SharedCount sharedCount = new SharedCount(curatorFramework, "/curator/sharecount", 100);
        // 启动共享计数器
        sharedCount.start();
        // 获取共享计数器的值
        int count = sharedCount.getCount();
        // 修改共享计数器的值
        int random = new Random().nextInt(100);
        sharedCount.setCount(random);
        log.info("共享计数器的初始值:{},现在修改成了:{}",count,random);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
DistributedAtomicNumber

​ DistributedAtomicNumber接口是分布式原子数值类型的抽象,定义了分布式原子数值类型需要提供的方法。

​ DistributedAtomicNumber接口有两个实现:DistributedAtomicLongDistributedAtomicInteger

image-20220711225708066

​ 这两个实现将各种原子操作的执行委托给了DistributedAtomicValue,所以这两种实现是类似的,只不过表示的数值类型不同而已。

​ DistributedAtomicLong除了计数的范围比SharedCount大了之外,比SharedCount更简单易用。它首先尝试使用乐观锁的方式设置计数器, 如果不成功(比如期间计数器已经被其它client更新了), 它使用InterProcessMutex方式来更新计数值。此计数器有一系列的操作:

  • get(): 获取当前值
  • increment():加一
  • decrement(): 减一
  • add():增加特定的值
  • subtract(): 减去特定的值
  • trySet(): 尝试设置计数值
  • forceSet(): 强制设置计数值

你必须检查返回结果的succeeded(), 它代表此操作是否成功。如果操作成功, preValue()代表操作前的值, postValue()代表操作后的值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/597294.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一题——重复的子字符串

每日一题 重复的子字符串 题目链接 注&#xff1a;本题的题解基本建立在KMP算法之上&#xff0c;对KMP算法不太了解的小伙伴可以参考这篇文章KMP算法及其改进图文详解 方法一&#xff1a;移动匹配 我们先来看几个可以由一个字串重复多次构成的主字符串&#xff1a;“aaa”&am…

chatgpt赋能Python-python人脸识别步骤

简介 Python是一种优秀的编程语言&#xff0c;它广泛应用于人工智能、数据科学、Web应用程序开发等领域。其中&#xff0c;人脸识别是Python应用程序中的重要一环。 本文将介绍Python人脸识别的具体步骤&#xff0c;希望对初学者有所帮助。 Python人脸识别步骤 安装必要的库…

销售管理全面指南:职能、流程、目标和工具

销售管理是优化公司销售队伍的过程&#xff0c;以有效利用现有资源来完成交易。对于任何依靠销售来推动收入的企业来说&#xff0c;这是绝对必要的。 销售管理可以细分为三个主要领域&#xff1a;销售运营、销售策略和销售分析。 根据美国营销协会&#xff08;AMA&#xff…

使用腾讯云短信服务实现Spring Boot短信发送

发送短信 01 注册微信公众号02 注册腾讯云账号03 创建签名和模板第一步 创建签名第二部 创建模板 04 发送短信 01 注册微信公众号 在百度中寻找微信公众平台&#xff0c;并在进入官网后注册。在账号注册过程中&#xff0c;需选择订阅号(个人)。注册成功后&#xff0c;请保存账…

我的浙大MEM提前批面试全流程重点信息梳理

浙江大学MEM已上岸&#xff0c;目前在读&#xff0c;给大家分享下我的备考经验&#xff0c;希望可以帮助到大家。 在确定自己的目标院校后收集相关信息是非常重要的&#xff0c;比如今年计划招多少学生&#xff0c;往年上岸都需要多少分&#xff0c;学费等情况&#xff0c;招考…

Kyligence x 集简云|无代码集成数百款应用,轻松打造数据产品

一站式指标平台 Kyligence Zen 现已支持对接集简云平台&#xff0c;企业无需繁琐的开发工作&#xff0c;即可无代码集成数百款应用&#xff0c;打破数据孤岛、统一数据口径&#xff0c;帮助企业实现数据的协作和分享&#xff0c;轻松进行数据分析、构建数据产品&#xff0c;助力…

腾讯董志强出席全国信安标委“标准周”:数字化转型需要高安全等级架构

2023年5月29日至6月1日&#xff0c;全国信息安全标准化技术委员会&#xff08;以下简称“信安标委”&#xff09;2023年第一次“标准周”活动在云南昆明举行。此次活动聚集了全国顶级的网络安全标准专家、学者和业界领袖&#xff0c;共同探讨网络安全标准领域的前沿议题和最佳实…

c语言函数返回值的几种方式,入参方式回传数据

背景 在正常使用过程中&#xff0c;突然发现有用二级指针传递地址&#xff0c;我想没必要用二级指针&#xff0c;实际目的是函数入参的参数&#xff0c;也是函数出参的参数 #mermaid-svg-ylOpK9fmaLgdD9YO {font-family:"trebuchet ms",verdana,arial,sans-serif;fo…

火山引擎A/B测试:MAB智能调优实验,企业活动效果提升新利器

618临近&#xff0c;各大电商APP的预热活动已然拉开序幕。对企业而言&#xff0c;一场活动从策划到上线&#xff0c;中间经过效果验证&#xff0c;其业务成本很高。一个好的活动创意从策划、开发、到最终发布&#xff0c;至少会经历几周实践&#xff0c;如果中间还经历A/B实验的…

三肽-33/Preventhelia(Diaminopropionoyl Tripeptide-33)

紫外线是一种电磁波&#xff0c;波长小于可见光&#xff0c;大部分地球表面的紫外线来自太阳&#xff0c;紫外线是伤害性光线的一种&#xff0c;经由皮肤吸收&#xff0c;会破坏DNA&#xff0c;使细胞会死亡或凋零从而产生皱纹、晒伤、等一系列不良反应。 作用机理---- Preven…

深入理解设计原则之依赖反转原则(DIP)

系列文章目录 C高性能优化编程系列 深入理解设计原则系列 深入理解设计模式系列 高级C并发线程编程 DIP&#xff1a;依赖反转原则 系列文章目录1、依赖反转原则的定义和解读2、稳定的抽象层3、依赖倒置原则和控制反转、依赖注入的联系小结 1、依赖反转原则的定义和解读 SOIL…

Linux零拷贝

零拷贝&#xff08;Zero-copy&#xff09;是一种优化技术&#xff0c;用于减少数据在内核空间和用户空间之间的拷贝次数&#xff0c;提高数据传输的效率和性能。它通过最小化数据的复制操作&#xff0c;将数据直接从源位置传输到目标位置&#xff0c;而不需要额外的数据拷贝。 …

微信小程序 构建npm报错: 没有找到可以构建的 NPM 包,请确认需要参与构建的 npm 都在 `miniprogramRoot` 目录内,

构建npm时报错 几个解决方案&#xff1a; 1、没有初始化项目 可以看这篇博客&#xff1a;微信小程序构建npm&#xff08;js和ts 2、ts版本下记得修改删除project.config.json中setting字段下的一些内容 需要加或修改 "packNpmManually": true, "packNpmRe…

短视频矩阵系统软件源码---技术部署创建

矩阵系统源码主要有三种框架&#xff1a;Spring、Struts和Hibernate。Spring框架是一个全栈式的Java应用程序开发框架&#xff0c;提供了IOC容器、AOP、事务管理等功能。Struts框架是一个MVC架构的Web应用程序框架&#xff0c;用于将数据模型、Web应用程序的用户界面和控制器逻…

实战助力未来|“饶派杯”XCTF车联网安全挑战赛圆满收官!

2023年5月31日&#xff0c;“饶派杯”XCTF车联网安全挑战赛于江西省上饶市圆满落幕。本次大赛由江西省委网信办、江西省工信厅、上饶市人民政府主办&#xff0c;旨在深入贯彻落实国家网络强国和交通强国战略部署&#xff0c;推动智能网联汽车技术与产业发展、加快该领域人才培养…

【Linux】基于环形队列的生产者消费者模型

文章目录 基于环形队列的生产消费模型生产者和消费者的关注点申请和释放资源的问题规则 RingQueue.hpp单生产者单消费者的生产者消费者模型:信号量保护环形队列的原理多生产者多消费者模型计算任务处理RingQueue.hppTask.hppRingQueue.cc 基于环形队列的生产消费模型 环形队列…

安装pytourch gpu并测试

输入nvidia-smi命令查看cuda版本号, 系统的CUDA版本决定了系统最高可以支持什么版本的cudatoolkit&#xff0c;它是向下兼容的, 可以装低版本但是不能装高版本。 更新下conda&#xff0c;用管理员打开cmd conda update -n base -c defaults conda 安装CUDATookit 使用以下命令…

8 指数族分布【手写+Xmind笔记】

文章目录 8 指数族分布【手写Xmind笔记】8.1 Xmind笔记8.2 手写证明 8 指数族分布【手写Xmind笔记】 8.1 Xmind笔记 8.2 手写证明

第十二篇、基于Arduino uno,获取多个按键的输入信号(滤波消抖)——结果导向

0、结果 说明&#xff1a;先来看看串口调试助手显示的结果&#xff0c;当按下按键的时候&#xff0c;按一次会打印一次按键被按下&#xff0c;并且打印是哪个按键被按下。如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;虽然每个型号的按键形态各异&a…

Linux---用户的权限

专栏&#xff1a;Linux 个人主页&#xff1a;HaiFan. 本章为大家带来用户的权限的讲解 用户的权限 Linux权限的概念权限的三类对象权限的三种类型权限设置chmod/chown/chgrp更改权限chmodchownchgrp umask目录的权限粘滞位 Linux权限的概念 Linux下有两种用户&#xff1a;超级…