分布式锁技术全景解析:从传统锁机制到MySQL、Redis/Redisson与ZooKeeper实现


文章目录

  • 一、分布式锁介绍
    • 1.1 为什么需要分布式锁
    • 1.2 什么是分布式锁?
    • 1.3 分布式锁特点
  • 二. 传统锁回顾
    • 2.1商品超卖演示
    • 2.2 JVM锁演示
    • 2.3 JVM锁失效的三种情况
      • 2.3.1 多例模式
      • 2.3.2 事务
      • 2.3.3 分布式集群
  • 三. 基于mysql实现分布式锁
    • 3.1 一条SQL
    • 3.2 悲观锁
    • 3.3 乐观锁
    • 3.4 总结
    • 基于mysql实现分布式锁
    • 2.1. 基本思路
    • 2.2. 代码实现
    • 2.3. 缺陷及解决方案
  • 四. 基于Redis实现分布式锁
    • 4.1 基本实现
    • 4.2 防死锁
    • 4.3 防误删
    • 4.4 使用lua保证删除原子性
  • 五. 使用Redisson实现分布式锁
  • 六. 基于zookeeper实现分布式锁
    • 方法概述
    • 示例代码
    • 推荐实践
    • 思维导图


一、分布式锁介绍

1.1 为什么需要分布式锁

在单机部署的系统中,使用线程锁来解决高并发的问题,多线程访问共享变量的问题达到数据一致性,如使用synchornized、ReentrantLock等;

但是在后端集群部署的系统中,程序在不同的JVM虚拟机中运行,且因为synchronized或ReentrantLock都只能保证同一个JVM进程中保证有效,所以这时就需要使用分布式锁了。

1.2 什么是分布式锁?

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的 一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享 了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
在这里插入图片描述

1.3 分布式锁特点

在这里插入图片描述

二. 传统锁回顾

2.1商品超卖演示

create table mall_stock (id int primary key auto_increment COMMENT '库存ID',product_id varchar(20) not null COMMENT '商品编号',sock_id int not null default 1 COMMENT '仓库ID',count int not null default 0 COMMENT '数量'
)

搭建环境

  • 压测
    下载地址:https://jmeter.apache.org/download_jmeter.cgi
    在这里插入图片描述
    双击jemter.bat运行软件

2.2 JVM锁演示

  • 在方法上添加同步关键字
  • ReentrantLock对象使用
ReentrantLock lock=new ReentrantLock();

在这里插入图片描述

2.3 JVM锁失效的三种情况

2.3.1 多例模式

业务对象或锁对象是多例的情况下

原因:业务中一般使用的lock对象锁,lock锁的范围是针对同一个对象里面不同的线程,也就是说,jvm锁是对象锁,对象之间锁不共用
在这里插入图片描述

@Scope("singleton") // prototype  原型模式(多例)singleton  单例模式(单例)
public class LockController

2.3.2 事务

在使用了spring事物注解的情况下(不单是jvm锁,大部分锁实现都会出现这个问题)

原因:spring事务是基于aop的方式实现的,是包裹着整个方法的(包括锁),事务不在锁的范围内,很容易出现并发执行的时候,a方法的事务还没提交上去,b事务就读了数据库的旧值。
在这里插入图片描述

在这里插入图片描述

2.3.3 分布式集群

原因:服务都不一样了,锁和对象自然也不一样(就和第一个情况下的环境一样)
解决方法:利用mysql的排他锁机制,将所有业务sql集中成一条sql(以上三种问题都能解决,但是不灵活,只能在业务允许的情况下使用)

总结
综上所述,我们可以发现jvm锁只适合在单体项目中并且业务需求简单的情况下使用,所以有条件还是使用分布式锁吧。

三. 基于mysql实现分布式锁

3.1 一条SQL

在这里插入图片描述
update ,insert,delete 写操作本身带排他锁

优点:一个sgl语句:更新数量时判断解决:解决了上面三个锁失效的问题

缺点
1 锁范围问题:是表级锁还是行级锁
一个sgl语句:更新数量时判断解决:解决了上面三个锁失效的问题,但是它是表级锁,这种是不能接受的,我要买多种商品结果你把表锁了,整张表都不能并发了、性能肯定就是不行的,最好使用行级锁。
mysql悲观锁中使用行级锁
1,锁的查询或者跟新条件必须是索引字段
2,查询或者更新条件必须是具体值
2.同一个商品有多条库存记录:仓库有多个、商品ID是一个,可以根据算法减库存、一个sql语句做不到
3.无法记录库存变化前后的状态

Mysql锁的区分
参考:https://blog.csdn.net/name_sakura/article/details/129286136

3.2 悲观锁

悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能被改动,一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。
数据库中的行锁,表锁,读锁,写锁均为悲观锁。

select ....  for update
service
@Transactional
public Boolean reduceStock(String productId, int count) {// 查询商品库存Stock stock = stockMapper.getStockByProductId(productId);if (stock!= null && stock.getCount() >= count) {// 减少库存stock.setCount(stock.getCount() - count);this.updateById(stock); // 更新库存return true;     // 减库存成功}return false;
}

mapper.xml

<select id="getStockByProductId" resultType="com.syh.model.entity.Stock">select <include refid="Base_Column_List"/>from mall_stockwhere product_id = #{productId} for update</select>

问题:
1,性能问题
2,死锁问题
3,库存操作要统一

3.3 乐观锁

乐观锁认为数据的变动不会太频繁。

乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。

事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本v1与数据中最新的版本v2相对比,如果v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时version会加1,以此来表明数据已被变动。

如果,v1不等于v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。不同于悲观锁,乐观锁通常是由开发者实现的。(CAS机制:Compare And Swap 比较并交换)

给表添加version字段
在这里插入图片描述
service方法

public Boolean reduceStock(String productId, int count) {// 查询商品库存Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>().eq("product_id", productId));if (stock != null && count >= 0) {stock.setCount(stock.getCount() - count);//改数量Integer version = stock.getVersion();   // 原版本号stock.setVersion(version + 1);//改版本号if(!this.update(stock,new QueryWrapper<Stock>().eq("product_id", productId).eq("version", version))){// 更新库存 更新失败重试try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}reduceStock(productId, count);}}return false;}

问题:
1,高并发情况下,性能极低
2,读写分离情况下导致乐观锁不可靠

3.4 总结

性能:一个sql>悲观锁>jvm锁>乐观锁
如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下。
优先选择:一个sql
如果写并发量较低(多读),争抢不是很激烈的情况下优先选择:乐观锁
如果写并发量较高,一般会经常冲突,此时选择乐观锁的话,会导致业务代码不间断的重试。
优先选择:mysql悲观锁
不推荐jvm本地锁。

基于mysql实现分布式锁

不管是jvm锁还是mysql锁,为了保证线程的并发安全,都提供了悲观独占排他锁。所以独占排他也是 分布式锁的基本要求。 可以利用唯一键索引不能重复插入的特点实现。设计表如下:

CREATE TABLE `db_lock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`lock_name` varchar(50) NOT NULL COMMENT '锁名',`class_name` varchar(100) DEFAULT NULL COMMENT '类名',`method_name` varchar(50) DEFAULT NULL COMMENT '方法名',`server_name` varchar(50) DEFAULT NULL COMMENT '服务器ip',`thread_name` varchar(50) DEFAULT NULL COMMENT '线程名',`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '获取锁时间',`desc` varchar(100) DEFAULT NULL COMMENT '描述',PRIMARY KEY (`id`),UNIQUE KEY `idx_unique` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;

Lock实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("db_lock")
public class Lock {private Long id;private String lockName;private String className;private String methodName;private String serverName;private String threadName;private Date createTime;private String desc;
}

LockMapper接口:

public interface LockMapper extends BaseMapper<Lock> {
}

2.1. 基本思路

synchronized关键字和ReetrantLock锁都是独占排他锁,即多个线程争抢一个资源时,同一时刻只有 一个线程可以抢占该资源,其他线程只能阻塞等待,直到占有资源的线程释放该资源。
在这里插入图片描述

  1. 线程同时获取锁(insert)
  2. 获取成功,执行业务逻辑,执行完成释放锁(delete)
  3. 其他线程等待重试

2.2. 代码实现

改造StockService:

@Servicepublic class StockService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate LockMapper lockMapper;/*** 数据库分布式锁*/public void checkAndLock() {// 加锁Lock lock = new Lock(null, "lock", this.getClass().getName(), new 
Date(), null);try {this.lockMapper.insert(lock);} catch (Exception ex) {// 获取锁失败,则重试try {Thread.sleep(50);this.checkAndLock();} catch (InterruptedException e) {e.printStackTrace();}}// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}// 释放锁this.lockMapper.deleteById(lock.getId());}
}

加锁:

// 加锁
Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
try {this.lockMapper.insert(lock);
} catch (Exception ex) {// 获取锁失败,则重试try {Thread.sleep(50);this.checkAndLock();} catch (InterruptedException e) {e.printStackTrace();}
}

解锁:

// 释放锁
this.lockMapper.deleteById(lock.getId());

2.3. 缺陷及解决方案

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
    解决方案:给锁数据库 搭建主备
  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
    解决方案:只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  3. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
    解决方案:记录获取锁的主机信息和线程信息,如果相同线程要获取锁,直接重入。
  4. 受制于数据库性能,并发能力有限。
    解决方案:无法解决。

四. 基于Redis实现分布式锁

4.1 基本实现

借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发 送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
在这里插入图片描述
● 1. 多个客户端同时获取锁(setnx)
● 2. 获取成功,执行业务逻辑,执行完成释放锁(del)
● 3. 其他客户端等待重试

@Service
public class StockService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate LockMapper lockMapper;@Autowiredprivate StringRedisTemplate redisTemplate;public void checkAndLock() {// 加锁,获取锁失败重试while (!this.redisTemplate.opsForValue().setIfAbsent("lock","xxx")){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}// 释放锁this.redisTemplate.delete("lock");}
}

4.2 防死锁

在这里插入图片描述
解决:给锁设置过期时间,自动释放锁。 设置过期时间两种方式:

  1. 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
  2. 使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)
    在这里插入图片描述

4.3 防误删

问题:可能会释放其他服务器的锁。 场景:如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。
  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
  3. index3获取到锁,执行业务逻辑
  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只 执行1s就被别人释放。 最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的 锁

在这里插入图片描述
在这里插入图片描述
问题:删除操作缺乏原子性。 场景:

  1. index1执行删除时,查询到的lock值确实和uuid相等
  2. index1执行删除前,lock刚好过期时间已到,被redis自动释放
  3. index2获取了lock 4. index1执行删除,此时会把index2的lock删除
    解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本)

4.4 使用lua保证删除原子性

lua脚本入门
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。

Redis 操作lua脚本
命令:EVAL命令
命令格式:EVAL script numkeys key [key …] arg [arg …]

script参数是一段 Lua5.1 脚本程序。脚本不必(也不应该[^1])定义为一个 Lua 函数
numkeys指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为0
key [key …] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
arg [arg …] 附加参数。在Lua脚本中通过ARGV[1],ARGV[2]获取。

删除LUA脚本:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', 
KEYS[1]) else return 0 end

代码实现:

public void checkAndLock() {// 加锁,获取锁失败重试String uuid = UUID.randomUUID().toString();while (!this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)){try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}// 释放锁String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return 
redis.call('del', KEYS[1]) else return 0 end";this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Arrays.asList("lock"), uuid);
}

五. 使用Redisson实现分布式锁

导包:

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.24.3</version>
</dependency>

配置:

package com.syh.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissionConfig {@Value("${spring.data.redis.host}")private String redisHost;@Value("${spring.data.redis.password}")private String password;@Value("${spring.data.redis.port}")private int port;@Beanpublic RedissonClient getRedisson() {System.out.println(redisHost+":"+port);Config config = new Config();config.useSingleServer().setAddress("redis://" + redisHost + ":" + port).setPassword(password);config.setCodec(new JsonJacksonCodec());return Redisson.create(config);}
}

使用:

 public Boolean reduceStock(String productId, int count) {RLock rLock = redissonClient.getLock("lock");try {boolean isLocked = rLock.tryLock(3, TimeUnit.SECONDS);if (isLocked) {// TODO// Reids 查询库存String stock = redisTemplate.opsForValue().get("count");if (stock != null && Integer.parseInt(stock) >= count) {redisTemplate.opsForValue().set("count", String.valueOf(Integer.parseInt(stock) - count));}}} catch (Exception e) { }finally {if (rLock.isHeldByCurrentThread()) {rLock.unlock();}}return false;}

六. 基于zookeeper实现分布式锁

方法概述

ZooKeeper 提供了一种可靠的机制来实现分布式锁,这有助于解决分布式系统中的并发控制问题。为了确保多个节点之间的操作一致性,可以通过创建临时顺序节点并利用其唯一性和有序性特点来构建锁定逻辑。

当应用程序请求获取锁时,会在指定路径下创建一个带有特定前缀的临时顺序节点;随后通过检查当前所创建节点是否是最小编号的那个节点(即第一个),如果是,则认为成功获得了锁;如果不是,则监听比自己序号小一位的节点变化事件,在前任节点消失之后再次尝试获取锁直到成为最小编号为止。

对于异常状况如网络分区或者服务器崩溃等情况下的处理也非常重要。例如连接断开、会话过期等问题都需要被妥善考虑以保障锁的安全性与可靠性。另外,设置合理的超时时间可以帮助预防潜在的死锁现象发生。

示例代码

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;public class SimpleDistributedLock {private static final String LOCK_ROOT_PATH = "/locks";public void acquireLock(ZooKeeper zk, String lockName) throws KeeperException, InterruptedException {String lockPathPrefix = LOCK_ROOT_PATH + "/" + lockName + "-";// Create ephemeral sequential node.String ourLockNode = zk.create(lockPathPrefix, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);while (true){List<String> childrenNodes = zk.getChildren(LOCK_ROOT_PATH, false);Collections.sort(childrenNodes); int index = childrenNodes.indexOf(zk.getState().toString());if(index == 0){ System.out.println(Thread.currentThread().getName() +" acquired the lock.");break;  }else{Stat predecessorStat = null;try {   String watchOnPredecessor = LOCK_ROOT_PATH +"/"+childrenNodes.get(index-1);zk.exists(watchOnPredecessor,true);synchronized(this){wait();}} catch(Exception e){}}}}public void releaseLock(){// Release logic here...System.out.println(Thread.currentThread().getName()+" released the lock.");}
}

此段程序展示了如何在一个给定名称空间内竞争一把互斥锁的过程。请注意实际应用中还需要加入更多健壮性的设计以及错误恢复机制等细节。

推荐实践

考虑到复杂度和维护成本,在真实环境中建议采用成熟的第三方库比如 Curator 来简化开发工作量。Curator 对于上述提到的各种类型的锁都提供了良好的支持,并且经过了广泛的测试验证能够很好地适应生产环境的需求。

思维导图

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【GPT入门】第18课 langchain介绍与API初步体验

【GPT入门】langchain第一课 langchain介绍与API初步体验 1. langchain介绍定义特点1. 模块化与灵活性2. 链式调用机制3. 数据连接能力4. 记忆管理功能5. 提示工程支持6. 可扩展性 2.langchain核心组件架构图3. 最简单的helloworld入门 1. langchain介绍 LangChain 是一个用于…

神经网络分类任务

import torch %matplotlib inline from pathlib import Path import requestsimport torchvision mnist_dataset torchvision.datasets.MNIST(root./data, downloadTrue) 下载mnist数据集 但不知道数据集里面是什么打印 import torchvision import torchvision.transforms …

ROS实践(三)机器人描述文件xacro(urdf扩展)

目录 一、定义 二、xacro 文件常见组成部分 1. 命名空间声明 2. 定义宏 3. 调用宏 4. 定义参数 5. 条件语句 6. 转换 xacro 文件为 urdf 7. gazebo标签 三、代码示例 1. gazebo标签使用&#xff08;仿真参数配置&#xff09; 2. 引用仿真配置并定义机器人模型&#x…

Vision Mamba论文精读笔记

这篇博客主要针对Vision Mamba 论文进行精读&#xff0c;包含全文翻译以及部分内容注解。 读者最好有SSM以及Mamba的前期基础&#xff0c;便于理解。 论文链接&#xff1a;[2401.09417] Vision Mamba: Efficient Visual Representation Learning with Bidirectional State Spa…

大模型架构记录4-文档切分 (chunks构建)

chunks&#xff1a; 块 trunks : 树干 “RAG”通常指 检索增强生成&#xff08;Retrieval-Augmented Generation&#xff09; 主要框架&#xff1a;用户提query&#xff0c;找到和它相关的&#xff0c;先把问题转换为向量&#xff0c;和向量数据库的数据做比较&#xff0c;检…

个性化音乐推荐系统

python、pycharm、Django、Mysql都已经安装好了&#xff01; 目录 2025/3/13 2025/3/13 一.打开CMD&#xff0c;安装Mysql驱动 pip install mysqlclient 二.项目初始化&#xff1a; 1.创建Django项目&#xff1a; django-admin startproject project1 cd project1 2.创…

面试高频#LeetCode#Hot100-字母异位词分组

题号链接 49. 字母异位词分组 - 力扣&#xff08;LeetCode&#xff09; 1首先定义map集合一个String对应一个String[]集合&#xff0c;遍历字符串数组 2对其先进行拆分&#xff0c;拆分为字符数组&#xff0c;再进行排序&#xff0c;再转为字符串 3如果key值没有就创建一个字符…

笔试刷题专题(一)

文章目录 最小花费爬楼梯&#xff08;动态规划&#xff09;题解代码 数组中两个字符串的最小距离&#xff08;贪心&#xff08;dp&#xff09;&#xff09;题解代码 点击消除题解代码 最小花费爬楼梯&#xff08;动态规划&#xff09; 题目链接 题解 1. 状态表示&#xff1…

hcia华为路由器静态路由实验配置

目录 一、网络拓扑分析 二、华为路由器配置&#xff08;分设备&#xff09; 1. R1 配置 2. R2 配置 3. R3 配置 三、验证测试 拓扑图 一、网络拓扑分析 IP 地址规划&#xff1a; R1&#xff1a;E0/0/0&#xff08;12.1.1.1/24&#xff09;、E0/0/1&#xff08;192.168.1.…

贪心算法和遗传算法优劣对比——c#

项目背景&#xff1a;某钢管厂的钢筋原材料为 55米&#xff0c;工作需要需切割 40 米&#xff08;1段&#xff09;、11 米&#xff08;15 段&#xff09;等 4 种规格 &#xff0c;现用贪心算法和遗传算法两种算法进行计算&#xff1a; 第一局&#xff1a;{ 40, 1 }, { 11, 15…

PowerBi,一个简单的动态度量值以及图表联动的案例

假设我们有一张[销量表]&#xff0c;数据如下: 我们想做下面的效果: 左边的饼图显示每个门店的销量以及百分比&#xff0c;右边是一个堆积条形图&#xff0c;显示每种商品的销量&#xff0c;并且有一个切片器能切换显示销售渠道 做法如下&#xff1a; 1.报表里放入一个饼图&a…

夜莺监控 v8.0 新版通知规则 | 对接企微告警

对新版本通知规则还不太了解的用户可以阅读文章&#xff1a;《夜莺监控巨大革新&#xff1a;抽象出通知规则&#xff0c;增强告警通知的灵活性》。下面我们将以企微通知为例&#xff0c;介绍如何使用新版通知规则来对接企微通知。 上图是通知规则对接企微通知的示意逻辑图。 在…

HCIA-11.以太网链路聚合与交换机堆叠、集群

链路聚合背景 拓扑组网时为了高可用&#xff0c;需要网络的冗余备份。但增加冗余容易后会出现环路&#xff0c;所以我们部署了STP协议来破除环路。 但是&#xff0c;根据实际业务的需要&#xff0c;为网络不停的增加冗余是现实需要的一部分。 那么&#xff0c;为了让网络冗余…

LeetCode 解题思路 15(Hot 100)

解题思路&#xff1a; 引入哑节点&#xff1a; 简化头节点删除操作&#xff0c;统一处理所有边界条件。快慢指针法&#xff1a; 快指针先移动 n 步&#xff0c;确保快慢指针距离为 n&#xff0c;之后同步移动快慢指针。当快指针到达末尾时&#xff0c;慢指针指向倒数第 n 个节…

大数据学习(65)- Hue详解

&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一…

设计模式之美

UML建模 统一建模语言&#xff08;UML&#xff09;是用来设计软件的可视化建模语言。它的语言特点是简单 统一 图形化 能表达软件设计中的动态与静态信息。 UML的分类 动态结构图&#xff1a; 类图 对象图 组件图 部署图 动态行为图&#xff1a; 状态图 活动图 时序图 协作…

【大模型学习】第十八章 强化学习介绍

目录 引言 一、 强化学习的理论基础与发展脉络 1.1 基本概念与核心要素 1.2 历史演进与里程碑 二、 强化学习的数学框架与核心算法 2.1 马尔可夫决策过程与贝尔曼方程 2.2 基于价值的算法 2.3 基于策略的算法 2.4 混合算法&#xff1a;Actor-Critic架构 2.5 应用举例 …

Chatbox通过百炼调用DeepSeek

解决方案链接&#xff1a;评测&#xff5c;零门槛&#xff0c;即刻拥有DeepSeek-R1满血版 方案概览 本方案以 DeepSeek-R1 满血版为例进行演示&#xff0c;通过百炼模型服务进行 DeepSeek 开源模型调用&#xff0c;可以根据实际需求选择其他参数规模的 DeepSeek 模型。百炼平台…

网络安全设备系统集成方案 系统集成和网络安全

一、网络安全概述 计算机网络安全是指计算机、网络系统的硬件、软件以及系统中的数据受到保护&#xff0c;不因偶然的或恶意的原因而遭到破坏、更改、泄露&#xff0c;确保系统能连续和可靠地运行&#xff0c;使网络服务不中断。广义地说&#xff0c;凡是涉及网络上信息的保密…

【菜鸟飞】通过vsCode用python访问公网deepseek-r1等模型(Tocken模式)

目标 通过vsCode用python访问deepseek。 环境准备 没有环境的&#xff0c;vscode环境准备请参考之前的文章&#xff0c;另外需安装ollama&#xff1a; 【菜鸟飞】用vsCode搭建python运行环境-CSDN博客 AI入门1&#xff1a;AI模型管家婆ollama的安装和使用-CSDN博客 选读文章…