Java中利用Redis,ZooKeeper,数据库等实现分布式锁(遥遥领先)

1. 分布式锁

1.1 什么是分布式锁

在我们进行单机应用开发涉及并发同步的时候,我们往往采用synchronized或者ReentrantLock的方式来解决多线程间的代码同步问题。但是当我们的应用是在分布式集群工作的情况下,那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题,这就是分布式锁。

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式锁可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保证数据的一致性。

分布式锁的实现方式有多种,例如:数据库实现方式、ZooKeeper实现方式、Redis实现方式等。

可能有人会问,例如synchronized、ReentrantLock不就能解决问题了吗?为什么还要使用分布式锁?

1.2 为什么要使用分布式锁

为了能够说明分布式锁的重要性,下面通过一个电商项目中减库存的案例来演示如果没有使用分布式锁会出现什么问题。代码如下:

第一步:导入坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>lock-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--集成redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>1.4.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

</project>

第二步:配置application.yml文件

server:
  port: 8080
spring:
  redis:
    host: 68.79.63.42
    port: 26379
    password: (you password)

第三步:编写Controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StockController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/stock")
    public String stock(){
        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            stock --;
            redisTemplate.opsForValue().set("stock",stock+"");
            System.out.println("库存扣减成功,剩余库存:" + stock);
        }else {
            System.out.println("库存不足!!!");
        }
        return "OK";
    }
}

第四步:编写启动类

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class,args);
    }
}

第五步:设置redis

在这里插入图片描述

测试方式:使用jmeter进行压力测试,如下:

在这里插入图片描述

在这里插入图片描述

注:Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。

查看控制台输出,发现已经出现了线程并发问题,如下:

在这里插入图片描述

由于当前程序是部署在一个Tomcat中的,即程序运行在一个jvm中,此时可以对减库存的代码进行同步处理,如下:

在这里插入图片描述

再次进行测试(注意恢复redis中的数据),此时已经没有线程并发问题,控制台输出如下:

在这里插入图片描述

这说明如果程序运行在一个jvm中,使用synchronized即可解决线程并发问题。

下面将程序进行集群部署(如下图所示),并通过Nginx进行负载,再进行测试。

在这里插入图片描述

操作过程:

第一步:配置Nginx

	upstream upstream_name{
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
    }
	
	server {
        listen       8080;
        server_name  localhost;

        location / {
            proxy_pass http://upstream_name;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

第二步:修改application.yml中端口号改为8001和8002并分别启动程序

第三步:使用jemter再次测试,可以看到又出现了并发问题

1.3 分布式锁应具有的特性
  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

2. 分布式锁实现方案

2.1 数据库实现分布式锁

基于数据库实现分布式锁的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引。想要执行某个方法,首先需要将这个方法名插入表中,成功插入则获取锁,执行完成后删除对应的行数据释放锁。此种方式就是建立在数据库唯一索引的特性基础上的。

表结构如下:

在这里插入图片描述

具体实现过程如下(在前面lock-test工程基础上进行改造):

第一步:在pom.xml中导入maven坐标

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

第二步:配置文件application.yml中配置mybatis-plus相关配置

server:
  port: 8002
spring:
  redis:
    host: 68.79.63.42
    port: 26379
    password: itheima123
  application:
    name: lockTest
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/dlock
      username: root
      password: root
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false
    auto-mapping-behavior: full
    #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml

第三步:创建实体类

package com.itheima.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;

@TableName("mylock")
public class MyLock implements Serializable {
    private int id;
    private String methodName;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
}

第四步:创建Mapper接口

package com.itheima.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.entity.MyLock;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MyLockMapper extends BaseMapper<MyLock> {
    public void deleteByMethodName(String methodName);
}

第五步:在resources/mapper目录下创建Mapper映射文件MyLockMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.MyLockMapper">
    <delete id="deleteByMethodName" parameterType="string">
        delete from mylock where methodName = #{value}
    </delete>
</mapper>

第六步:改造StockController

@Autowired
private MyLockMapper myLockMapper;

@GetMapping("/stock")
public String stock(){
    MyLock entity = new MyLock();
    entity.setMethodName("stock");

    try {
        //插入数据,如果不抛异常则表示插入成功,即获得锁
        myLockMapper.insert(entity);
        
        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            stock --;
            redisTemplate.opsForValue().set("stock",stock+"");
            System.out.println("库存扣减成功,剩余库存:" + stock);
        }else {
            System.out.println("库存不足!!!");
        }
        
        //释放锁
        myLockMapper.deleteByMethodName("stock");
    }catch (Exception ex){
        System.out.println("没有获取锁,不能执行减库存操作!!!");
    }

    return "OK";
}

通过观察控制台输出可以看到,使用此种方式已经解决了线程并发问题。

注意,虽然使用数据库方式可以实现分布式锁,但是这种实现方式还存在如下一些问题:

1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;

2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;

3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

2.2 ZooKeeper实现分布式锁

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。

Zookeeper中节点分为4种类型:

1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在

2.持久顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号

3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

Zookeeper实现分布式锁的原理是基于Zookeeper的临时顺序节点,如下图:

在这里插入图片描述

客户端A和客户端B争抢分布式锁,其实就是在/my_lock节点下创建临时顺序节点,这个顺序节点由zk内部自行维护一个节点序号,序号最小则表示对应客户端获得锁。

Apache Curator是一个比较完善的ZooKeeper客户端框架,通过封装的一套高级API 简化了ZooKeeper的操作,其中就包括分布式锁的实现。

具体操作过程如下(在前面lock-test工程基础上进行改造):

第一步:在pom.xml中导入maven坐标

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.10</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>

第二步:编写配置类

package com.itheima.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;

@Configuration
public class ZkConfig {
    @Bean
    public CuratorFramework curatorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(5000)
                .retryPolicy(retryPolicy)
                .build();
        client.start();
        return client;
    }
}

第三步:改造StockController

@Autowired
private CuratorFramework curatorFramework;

@GetMapping("/stock")
public String stock() {
    InterProcessMutex mutex = new InterProcessMutex(curatorFramework,"/mylock");

    try {
        //尝试获得锁
        boolean locked = mutex.acquire(0, TimeUnit.SECONDS);
        if(locked){
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if(stock > 0){
                stock --;
                redisTemplate.opsForValue().set("stock",stock+"");
                System.out.println("库存扣减成功,剩余库存:" + stock);
            }else {
                System.out.println("库存不足!!!");
            }
            //释放锁
            mutex.release();
        }else{
            System.out.println("没有获取锁,不能执行减库存操作!!!");
        }
    }catch (Exception ex){
        System.out.println("出现异常!!!");
    }

    return "OK";
}

通过观察控制台输出可以看到,使用此种方式已经解决了线程并发问题。

2.3 Redis实现分布式锁

redis实现分布式锁比较简单,就是调用redis的set命令设置值,能够设置成功则表示加锁成功,即获得锁,通过调用del命令来删除设置的键值,即释放锁。

2.3.1 版本一

加锁命令:set lock_key lock_value NX

解锁命令:del lock_key

Java程序:

@GetMapping("/stock")
public String stock() {
    try {
        //尝试加锁
        Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", "mylock");
        if(locked){//加锁成功
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if(stock > 0){
                stock --;
                redisTemplate.opsForValue().set("stock",stock+"");
                System.out.println("库存扣减成功,剩余库存:" + stock);
            }else {
                System.out.println("库存不足!!!");
            }
            //释放锁
            redisTemplate.delete("mylock");
        }else{
            System.out.println("没有获取锁,不能执行减库存操作!!!");
        }
    }catch (Exception ex){
        System.out.println("出现异常!!!");
    }

    return "OK";
}
2.3.2 版本二

上面版本一的实现中存在一个问题,就是当某个线程获得锁后程序挂掉,此时还没来得及释放锁,这样后面所有的线程都无法获得锁了。为了解决这个问题可以在加锁时设置一个过期时间防止死锁。

加锁命令:set lock_key lock_value NX PX 5000

解锁命令:del lock_key

Java程序:

@GetMapping("/stock")
public String stock() {
    try {
        //尝试加锁
        Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", "mylock",5000,TimeUnit.MILLISECONDS);
        if(locked){//加锁成功
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if(stock > 0){
                stock --;
                redisTemplate.opsForValue().set("stock",stock+"");
                System.out.println("库存扣减成功,剩余库存:" + stock);
            }else {
                System.out.println("库存不足!!!");
            }
            //释放锁
            redisTemplate.delete("mylock");
        }else{
            System.out.println("没有获取锁,不能执行减库存操作!!!");
        }
    }catch (Exception ex){
        System.out.println("出现异常!!!");
    }

    return "OK";
}
2.3.3 版本三

针对前面版本二还有一点需要优化,就是加锁和解锁必须是同一个客户端,所以在加锁时可以设置当前线程id,在释放锁时判断是否为当前线程加的锁,如果是再释放锁即可。

Java程序:

@GetMapping("/stock")
public String stock() {
    try {
        String threadId = Thread.currentThread().getId()+"";
        //尝试加锁
        Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock",threadId,5000,TimeUnit.MILLISECONDS);
        if(locked){//加锁成功
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if(stock > 0){
                stock --;
                redisTemplate.opsForValue().set("stock",stock+"");
                System.out.println("库存扣减成功,剩余库存:" + stock);
            }else {
                System.out.println("库存不足!!!");
            }
            String myValue = redisTemplate.opsForValue().get("mylock");
            if(threadId.equals(myValue)){
                //释放锁
                redisTemplate.delete("mylock");
            }
        }else{
            System.out.println("没有获取锁,不能执行减库存操作!!!");
        }
    }catch (Exception ex){
        System.out.println("出现异常!!!");
    }

    return "OK";
}

3. Redisson

3.1 Redisson介绍

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

Redisson已经内置提供了基于Redis的分布式锁实现,此种方式是我们推荐的分布式锁使用方式。

Redisson的maven坐标如下:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.1</version>
</dependency>
3.2 Redisson分布式锁使用方式

第一步:在pom.xml中导入redisson的maven坐标

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.1</version>
</dependency>

第二步:编写配置类

package com.itheima.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
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 RedissonConfig {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        config.useSingleServer().setPassword(password);
        final RedissonClient client = Redisson.create(config);
        return client;
    }
}

第三步:改造Controller

@Autowired
private RedissonClient redissonClient;

@GetMapping("/stock")
public String stock() {
    //获得分布式锁对象,注意,此时还没有加锁成功
    RLock lock = redissonClient.getLock("mylock");
    try {
        //尝试加锁,如果加锁成功则后续程序继续执行,如果加锁不成功则阻塞等待
        lock.lock(5000,TimeUnit.MILLISECONDS);

        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if(stock > 0){
            stock --;
            redisTemplate.opsForValue().set("stock",stock+"");
            System.out.println("库存扣减成功,剩余库存:" + stock);
        }else {
            System.out.println("库存不足!!!");
        }
    }catch (Exception ex){
        System.out.println("出现异常!!!");
    }finally {
        //解锁
        lock.unlock();
    }

    return "OK";
}
3.3 Lua脚本
3.3.1 Lua简介

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

从Redis2.6.0版本开始提供了EVAL 和 EVALSHA 命令,这两个命令可以执行 Lua 脚本。

3.3.2 Redis中使用Lua的好处

Redis中使用Lua的好处:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延
  • 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务
  • 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑
3.3.3 如何在Redis中使用Lua

在redis中使用Lua脚本主要有三个命令

  • eval
  • evalsha
  • script load

eval用来直接执行lua脚本,使用方式如下:

EVAL script numkeys key [key ...] arg [arg ...]

key代表要操作的redis key

arg可以传自定义的参数

numkeys用来确定key有几个

script就是你写的lua脚本

lua脚本里面使用KEYS[1]和ARGV[1]来获取传递的第一个key和第一个arg,后面以此类推

举例:

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 city beijing
eval "return redis.call('set','name','xiaoming')" 0
eval "return redis.call('del',KEYS[1])" 1 city
eval "return redis.call('get',KEYS[1])" 1 name
eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[2], ARGV[1]); redis.call('expire', KEYS[2], ARGV[2]);return nil;end;" 2 citys city beijing 5000

在用eval命令的时候,可以注意到每次都要把执行的脚本发送过去,这样势必会有一定的网络开销,所以redis对lua脚本做了缓存,通过script load 和 evalsha实现:

SCRIPT LOAD script
EVALSHA sha1 numkeys key [key ...] arg [arg ...]

举例:

script load "return redis.call('get',KEYS[1]);"
evalsha 0e11c9f252fd76115c38403ce6095872b8c70580 1 name

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【2023年终总结】 | 时光之舟:乘载着回忆与希望穿越2023,抵达2024

文章目录 1 回忆2 希望 1 回忆 2023年对我来说是非常梦幻的一年&#xff0c;我在2023年初的时候确认去做AI方向&#xff0c;在这之前我尝试了前端开发&#xff0c;移动App开发&#xff0c;云FPGA等方向&#xff0c;但是感觉自己都不是很喜欢&#xff0c;然后就开始尝试新的方向…

2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项样题卷③

2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项&#xff08;高职组&#xff09; 样题&#xff08;第3套&#xff09; 目录 2023年“中银杯”四川省职业院校技能大赛“云计算应用”赛项&#xff08;高职组&#xff09; 样题&#xff08;第3套&#xff09; 模块…

vmware安装openEuler 22.03 LTS操作系统

vmware安装openEuler 22.03 LTS操作系统 1、下载openEuler操作系统镜像文件2、安装openEuler操作系统3、配置openEuler操作系统3.1、配置静态IP地址 和 dns3.2、查看磁盘分区3.3、查看系统版本 1、下载openEuler操作系统镜像文件 官网下载链接 链接: https://www.openeuler.or…

010、切片

除了引用&#xff0c;Rust还有另外一种不持有所有权的数据类型&#xff1a;切片&#xff08;slice&#xff09;。切片允许我们引用集合中某一段连续的元素序列&#xff0c;而不是整个集合。 考虑这样一个小问题&#xff1a;编写一个搜索函数&#xff0c;它接收字符串作为参数&a…

QT上位机开发(图形绘制)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 图形绘制是上位机软件开发很重要的一个功能。这个图形绘制&#xff0c;有的是离线的&#xff0c;有的是实时绘制的。就我个人而言&#xff0c;离线…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK获取相机当前数据吞吐量(C#)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK里函数来获取相机当前数据吞吐量&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的数据吞吐量的技术背景CameraExplorer如何查看相机吞吐量信息在NEOAPI SDK里通过函数获取相机接口吞吐量 Baumer工业相机通过NEOAPISDK获…

美团到店终端从标准化到数字化的演进之路

总第580篇 | 2023年第032篇 本文整理自美团技术沙龙第76期《大前端研发协同效能提升与实践》。前端团队在产研多角色协同形式上存在不同阶段&#xff0c;而大前端多技术栈在各阶段都有其独特的实践&#xff0c;同时又有类似的演进路线。本文从到店终端团队移动端和前端技术栈持…

用python做猴子摘桃的题目,java猴子爬台阶算法

本篇文章给大家谈谈猴子爬山算法java完整代码&#xff0c;以及用python做猴子摘桃的题目&#xff0c;希望对各位有所帮助&#xff0c;不要忘了收藏本站喔。 """ 一天一只猴子想去从山脚爬到山顶&#xff0c;途中经过一个有N个台阶的阶梯&#xff0c;但是这猴子有…

Embedding模型在大语言模型中的重要性

引言 随着大型语言模型的发展&#xff0c;以ChatGPT为首&#xff0c;涌现了诸如ChatPDF、BingGPT、NotionAI等多种多样的应用。公众大量地将目光聚焦于生成模型的进展之快&#xff0c;却少有关注支撑许多大型语言模型应用落地的必不可少的Embedding模型。本文将主要介绍为什么…

12 HAL库的硬件SPI驱动数码管

引言&#xff1a; 本文将为大家介绍一下SPI&#xff0c; 数码管的知识&#xff0c; 以及HAL库驱动SPI接口的数码的代码示例。 一、SPI的基础知识 1. SPI简介 01 SPI是串行外设接口&#xff08;Serial Peripheral Interface&#xff09;的缩写 02 是美国摩托罗拉公司&#xff08…

5个用于构建Web应用程序的Go Web框架

探索高效Web开发的顶级Go框架 Go&#xff08;或称为Golang&#xff09;以其简洁性、高效性和出色的标准库而闻名。然而&#xff0c;有几个流行的Go Web框架和库为构建Web应用程序提供了额外的功能。以下是五个最值得注意的Go框架&#xff1a; 1. Gin&#xff1a; Gin是一个高…

加法器原理详解

加法器的介绍与原理分析 什么是加法器&#xff1f; 加法器是一种数字电路&#xff0c;用于将两个二进制数相加并输出它们的和。 如何实现加法器 要讨论如何实现加法器就要先从只有一位的数字先进行考虑 一位二进制数相加 不考虑来自低位的进位——半加器 对于一位二进制…

训狗技术从初级到高级,专业有效的训狗训犬教程

一、教程描述 现在大部分人家里都会养些宠物&#xff0c;比如狗狗&#xff0c;虽然狗狗的一些行为习惯跟遗传有关&#xff0c;但是主人后天的影响也会给狗狗带来改变&#xff0c;本套教程教你纠正狗狗的不良行为&#xff0c;可以让你与狗愉快地玩耍。本套训狗教程&#xff0c;…

hugo-theme-kiwi V0.0.2 博客主题上新了时间轴

至此佳节&#xff0c;我在此给正在屏幕前浏览本文的您和您的家人&#xff0c;恭祝元旦快乐&#xff0c;虽然&#xff0c;这声祝福是晚了&#xff0c;但却不妨碍我我由内心深处对您和您的家人的诚挚祝福&#xff01; 新的一年&#xff0c;从这一天逐渐步入我们的生活&#xff0c…

你好2024!

大家好&#xff0c;我是小悟 2024年1月1日&#xff0c;新年的第一天&#xff0c;阳光明媚&#xff0c;空气中弥漫着希望和新的开始的气息。在这个特别的日子里&#xff0c;大家纷纷走出家门&#xff0c;迎接新年的到来。 街道上&#xff0c;熙熙攘攘的人群中&#xff0c;有孩…

个人博客主题 vuepress-hope

文章目录 1. 简介2. 配置2.1 个人博客&#xff0c;社媒链接配置 非常推荐vuepress-hope 1. 简介 下面的我的博客文章的截图 通过md写博客并且可以同步到github-page上 2. 配置 2.1 个人博客&#xff0c;社媒链接配置 配置文件 .vuepress/theme.ts blog: {medias: {BiliB…

B2005 字符三角形(python)

a input() print( a) print( a a a) print(a a a a a)python中默认输入的是字符型&#xff0c;第一句就是输入了一个字符赋给a python中单引号内的也是字符串&#xff0c;用print输出需要连接的字符串时用加号加在后面即可

SELinux 基本原理

本文讲述 SELinux 保护安全的基本原理 首发公号&#xff1a;Rand_cs 安全检查顺序 不废话&#xff0c;直接先来看张图 当我们执行系统调用的时候&#xff0c;会首先对某些错误情况进行检查&#xff0c;如果失败通常会得到一些 error 信息&#xff0c;通过查看全局变量 errno …

Primavera Unifier 项目控制延伸:Phase Gate理论:2/3

阶段Gate的具体内容&#xff1a; 阶段0 根据公司需要和资源现状&#xff0c;决定开展哪些项目。在这个阶段&#xff0c;公司一般需要开展一些脑力风暴或者团队集思广益的活动以获得足够多的点子。一旦团队决定采用某个想法&#xff0c;必须从各个维度去完善它&#xff0c;并使…

软件测试/测试开发丨接口测试之Postman 安装与使用

Postman 安装 官网下载地址 www.postman.com/downloads Postman 使用 发送get请求 新建请求 填写请求方式&#xff1a;GET 填写请求 URL&#xff1a; ceshiren.com/httpbin.ceshiren.com/get 填写请求参数&#xff1a; para_key para_value 发送 POST 请求 请求方式&…