全局唯一id生成器 各种实现记录

news2024/11/18 2:37:32

全局唯一id生成器

Redis 生成

前提知识~~ 时间戳

时间戳这个东西我们老是听到,却可能不是特别了解
首先,时间戳是从1970年1月1号0点0分开始的秒数,我查了蛮多资料,理论上来说,时间戳是没有上限的,而我们一般用位数来限制这里的上限,比如32位

我们来实际计算一下
32位的二进制, 2的32次方 - 1 = 4294967296 - 1 = 4294967295
因为时间戳表示的是秒数,所以这里就是32位下,最大的秒数

一天的秒数为 86400
365天的秒数为31536000

那么32位的时间戳是 4294967295 / 31536000 = 136

像现在是2024年,已经过了54年了,那么还有82年就要过期了

搞清楚这里的计算,我们后面就不会突然觉得,诶这里会不会超出上限

如何实现Redis全局id

首先我们要搞清楚为什么要全局id,全局id的作用是什么

第一: 唯一性,我们不能一套系统很多种全局id的生成器把,不能都用mysql自动生成id把,那样会混论,尤其是分布式系统
第二: 安全性,为了不让黑客知道我们生成id的规律,我们要加点佐料进去,例如时间戳
第三: 高可用 + 高性能 + 递增性 高可用就是,一个单点故障了,另外的一个服务器可以顶上,高性能就是生成的快,递增性就是为了我们业务的正常递增

所以我们就有redis生成全局id
这上面都符合,特别是高可用,可以用redis集群来保证,但是安全性,就要用不同的方法来实现了

这里是一种设计方法

设计的详解

在这里插入图片描述
时间戳31位,序列号32位

这里的全局id的意思就是,每一秒内的序列号作为全局id

这里的设计就很不错,这样很大程度上解决了问题,你可能会想要是1s内,超出了2的32次方怎么办,好办,就多写几位,压缩时间戳的位数

我们再来讲讲这里的时间戳的上限,如果是31位的化,那么最大就是2的31次方- 1 = 2147483648 - 1 = 2147483647
一年的秒数(365天) = 2147483647

2147483647 / 31536000 = 68 年 约等于68年, 现在是2024年 离1970年已经54年了,所以按道理来说14年后就过期了 也就是2038年

这里的序列号,就用redis的自增来实现

实际代码

/**
 * 全局唯一id生成器 Redis实现
 * @author jjking
 * @date 2024-02-07 20:27
 */
@Component
public class RedisIdWorker {
    
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 生成id
     * @param keyPrefix 业务的前缀key
     * @return
     */
    public long nextId(String keyPrefix) {
        //生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long timestamp = now.toEpochSecond(ZoneOffset.UTC);

        //生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = redisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        return timestamp << 32 | count;
    }
}

这里比较有意思的点是两个点
第一: 是这里的redis生成序列号的点,特别要加入业务代码的前缀,不然全都用一个不就乱了套了,还有就是redis的value是有位数上限的,好像是2的64次方,所以这里还是会超出上限的,那么为了解决这个问题,就用了这里的时间来做区别,这样基本就不会有问题了

第二: ,这里的返回结果的计算也蛮有意思,首先是时间的位数向左边移动了32位,这里的意思就是腾出32位给序列号,然后再用位运算 或,来加上这里的序列号

特别要注意这里的或,很有意思,0 | 0 还是0 0 | 1 那么就是1,所以这里可以直接加上,这个得想一想才能想明白

UUID生成

UUID就比较耳熟能祥了,我这里写一个生成的范例

@Test
public void test1() {
    String uuid = UUID.randomUUID().toString();
    System.out.println(uuid);
}

在这里插入图片描述
可以看出来,他的位数分布是8-4-4-4-12位,一共是32位16进制数

我们来计算一下,总共多少字节,我们先转为为二进制,一位16进制,是4位二进制,那么 总共有32 * 4 = 128位二进制 一个字节是8位二进制

128 / 8 = 16字节

我们上面的redis生成的id是64位的,他的一半8个字节

所以,他的第一个缺点就是太大了,占内存

而且,这个uuid,也不太安全

但是他的优点就是性能还算蛮高的,还没有网络消耗

雪花算法 (重中之重)

先来了解雪花算法生成的id组成

  • 最高位 固定为 0 ,符号位,因为生成的id都为正数,固定为0
  • 41位 时间戳 单位 毫秒 经过计算最多可以使用69年
  • 10 位机器码 = 5位 数据中心id + 5位 工作机器id
  • 12 位序列号

这个样子有点类似于我们redis生成的id,不过序列号少了,并且是毫秒级的,还有一个机器码

我这里摘的是糊涂工具包中的雪花算法id,并且简略了一些无关辅助代码

代码

package com.hmdp.utils;

import cn.hutool.core.date.SystemClock;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;

import java.io.Serializable;
import java.util.Date;

/**
 * Twitter的Snowflake 算法<br>
 * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。
 *
 * <p>
 * snowflake的结构如下(每部分用-分开):<br>
 *
 * <pre>
 * 符号位(1bit)- 时间戳相对值(41bit)- 数据中心标志(5bit)- 机器标志(5bit)- 递增序号(12bit)
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 * </pre>
 * <p>
 * 第一位为未使用(符号位表示正数),接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>
 * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)<br>
 * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
 * <p>
 * 并且可以通过生成的id反推出生成时间,datacenterId和workerId
 * <p>
 * 参考:http://www.cnblogs.com/relucent/p/4955340.html<br>
 * 关于长度是18还是19的问题见:https://blog.csdn.net/unifirst/article/details/80408050
 *
 * @author Looly
 * @since 3.0.1
 */
public class Snowflake implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 默认的起始时间,为Thu, 04 Nov 2010 01:42:54 GMT
	 */
	public static long DEFAULT_TWEPOCH = 1288834974657L;
	/**
	 * 默认回拨时间,2S
	 */
	public static long DEFAULT_TIME_OFFSET = 2000L;

	private static final long WORKER_ID_BITS = 5L;



	// 最大支持机器节点数0~31,一共32个
	@SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
	//-1L 为 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 (1L的补码)
	//左移5为  1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1110 0000
	//-1L为   1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
	// ^ 异或是不同为1,相同为0
	// 结果为  0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111
	private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);


	private static final long DATA_CENTER_ID_BITS = 5L;


	// 最大支持数据中心节点数0~31,一共32个
	@SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
	//和上面的最大工作id一样的道理
	private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);



	// 序列号12位(表示只允许workId的范围为:0-4095)
	private static final long SEQUENCE_BITS = 12L;
	// 机器节点左移12位
	private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
	// 数据中心节点左移17位
	private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
	// 时间毫秒数左移22位
	private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;


	// 序列掩码,用于限定序列最大值不能超过4095
	//计算机的负数是用补码表示的
	//1L     0000000000000000000000000000000000000000000000000000000000000001
	//1L 反码 1111111111111111111111111111111111111111111111111111111111111110
	//1L 补码 1111111111111111111111111111111111111111111111111111111111111111      补码 = 反码 + 1

	//这里是 -1L(64位1) 往左移动12位 111111111111111111111111111111111111111111111111 0000 0000 0000 0000
	// ~ 取反 					  000000000000000000000000000000000000000000000000 1111 1111 1111 1111
	//结果为                       2的12次方 - 1 = 4095
	@SuppressWarnings("FieldCanBeLocal")
	private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 4095

	//起始时间
	private final long twepoch;


	private final long workerId;
	private final long dataCenterId;
	private final boolean useSystemClock;
	// 允许的时钟回拨数
	private final long timeOffset;

	private long sequence = 0L;
	private long lastTimestamp = -1L;


	/**
	 * @param epochDate        初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
	 * @param workerId         工作机器节点id
	 * @param dataCenterId     数据中心id
	 * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
	 * @param timeOffset 允许时间回拨的毫秒数
	 * @since 5.7.3
	 */
	public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {

		//如果没有给起始的时间就用默认的起始时间
		if (null != epochDate) {
			this.twepoch = epochDate.getTime();
		} else{
			// Thu, 04 Nov 2010 01:42:54 GMT
			this.twepoch = DEFAULT_TWEPOCH;
		}

		//工作机器id <= 31
		if (workerId > MAX_WORKER_ID || workerId < 0) {
			throw new IllegalArgumentException(StrUtil.format("worker Id can't be greater than {} or less than 0", MAX_WORKER_ID));
		}
		if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
			throw new IllegalArgumentException(StrUtil.format("datacenter Id can't be greater than {} or less than 0", MAX_DATA_CENTER_ID));
		}


		this.workerId = workerId;
		this.dataCenterId = dataCenterId;
		this.useSystemClock = isUseSystemClock;
		this.timeOffset = timeOffset;
	}

	/**
	 * 根据Snowflake的ID,获取机器id
	 *
	 * @param id snowflake算法生成的id
	 * @return 所属机器的id
	 */
	public long getWorkerId(long id) {
		return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);
	}

	/**
	 * 根据Snowflake的ID,获取数据中心id
	 *
	 * @param id snowflake算法生成的id
	 * @return 所属数据中心
	 */
	public long getDataCenterId(long id) {
		return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);
	}

	/**
	 * 根据Snowflake的ID,获取生成时间
	 *
	 * @param id snowflake算法生成的id
	 * @return 生成的时间
	 */
	public long getGenerateDateTime(long id) {
		return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;
	}

	/**
	 * 下一个ID
	 *
	 * @return ID
	 */
	public synchronized long nextId() {
		//获取当前时间戳
		long timestamp = genTime();

		//如果小于上次的时间,这里有问题,时间回拨!
		if (timestamp < this.lastTimestamp) {
			if(this.lastTimestamp - timestamp < timeOffset){
				// 容忍指定的回拨,避免NTP校时造成的异常
				timestamp = lastTimestamp;
			} else{
				// 如果服务器时间有问题(时钟后退) 报错。
				throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
			}
		}


		//如果等于上次的时间,说明,此时是同一毫秒,递增序列号
		if (timestamp == this.lastTimestamp) {

			//SEQUENCE_MASK为4095,这里的运算看上面的解释,这个相当于最大值
			//SEQUENCE_MASK 为                             00000000000000000000000000000000000000000000 0000 1111 1111 1111 1111
			//假设此时的序列号为4095(sequence) 那么前面是4096   00000000000000000000000000000000000000000000 0001 0000 0000 0000 0000
			//这样子与,0 & 0 = 0 ----- 0 & 1 = 0 ----- 1 & 1 = 1
			//所以最后结果为 								   00000000000000000000000000000000000000000000 0000 0000 0000 0000 0000
			final long sequence = (this.sequence + 1) & SEQUENCE_MASK;


			//如果此时为0说明,已经到了4095了,到达上限,应该等待下一个毫秒
			if (sequence == 0) {
				timestamp = tilNextMillis(lastTimestamp);
			}

			this.sequence = sequence;
		} else {
			sequence = 0L;
		}

		//赋值此时的上一次时间戳(毫秒)
		lastTimestamp = timestamp;



		return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
				| (dataCenterId << DATA_CENTER_ID_SHIFT)
				| (workerId << WORKER_ID_SHIFT)
				| sequence;
	}

	/**
	 * 循环等待下一个时间
	 *
	 * @param lastTimestamp 上次记录的时间
	 * @return 下一个时间
	 */
	private long tilNextMillis(long lastTimestamp) {
		long timestamp = genTime();
		// 循环直到操作系统时间戳变化
		while (timestamp == lastTimestamp) {
			timestamp = genTime();
		}
		if (timestamp < lastTimestamp) {
			// 如果发现新的时间戳比上次记录的时间戳数值小,说明操作系统时间发生了倒退,报错
			throw new IllegalStateException(
					StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
		}
		return timestamp;
	}

	/**
	 * 生成时间戳
	 *
	 * @return 时间戳
	 */
	private long genTime() {
		return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
	}
	// ------------------------------------------------------------------------------------------------------------------------------------ Private method end
}

会有点长,但是核心的东西就一段

我们直接来看这一段

	/**
	 * 下一个ID
	 *
	 * @return ID
	 */
	public synchronized long nextId() {
		//获取当前时间戳
		long timestamp = genTime();

		//如果小于上次的时间,这里有问题,时间回拨!
		if (timestamp < this.lastTimestamp) {
			if(this.lastTimestamp - timestamp < timeOffset){
				// 容忍指定的回拨,避免NTP校时造成的异常
				timestamp = lastTimestamp;
			} else{
				// 如果服务器时间有问题(时钟后退) 报错。
				throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
			}
		}


		//如果等于上次的时间,说明,此时是同一毫秒,递增序列号
		if (timestamp == this.lastTimestamp) {

			//SEQUENCE_MASK为4095,这里的运算看上面的解释,这个相当于最大值
			//SEQUENCE_MASK 为                             00000000000000000000000000000000000000000000 0000 1111 1111 1111 1111
			//假设此时的序列号为4095(sequence) 那么前面是4096   00000000000000000000000000000000000000000000 0001 0000 0000 0000 0000
			//这样子与,0 & 0 = 0 ----- 0 & 1 = 0 ----- 1 & 1 = 1
			//所以最后结果为 								   00000000000000000000000000000000000000000000 0000 0000 0000 0000 0000
			final long sequence = (this.sequence + 1) & SEQUENCE_MASK;


			//如果此时为0说明,已经到了4095了,到达上限,应该等待下一个毫秒
			if (sequence == 0) {
				timestamp = tilNextMillis(lastTimestamp);
			}

			this.sequence = sequence;
		} else {
			sequence = 0L;
		}

		//赋值此时的上一次时间戳(毫秒)
		lastTimestamp = timestamp;



		return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
				| (dataCenterId << DATA_CENTER_ID_SHIFT)
				| (workerId << WORKER_ID_SHIFT)
				| sequence;
	}

我们来总结一下,这个核心代码的代码逻辑

我们要生成id的化,需要几部分 时间戳 + 机器码 + 序列号
机器码也就是我们服务器的标识,一般是我们字节写的,所以不用考虑这个

重点在于时间戳 + 序列号


时间戳的生成: 当前时间戳,并且是毫秒级的
时间戳的生成,代码很简单,所以也不要终点考虑


序列号的生成(重点): 第一: 我们需要校验这里的时间戳,是否有问题,也就是当前时间比上一次的时间还早,出现时间回拨问题

第二: 我们得校验此时的序列号是否超过上限,如果超过上限,那么置此时的序列号为0,并且等待下一毫秒,将此时的时间戳更新

最重要的问题就是这两,相比较,比较简单的问题是这里的超过上限问题,这里也很简单,就是循环等待下一毫秒,到达下一毫秒更新此时的时间戳,序列号也已经设置好了为0

最难也是最重要的问题,时间回拨问题,这里的位运算问题,还是很好理解的,只要会位运算,都能解决

但是我这里特别不能搞懂,为啥这里要用位运算

类似于如下代码

	// 序列掩码,用于限定序列最大值不能超过4095
	//计算机的负数是用补码表示的
	//1L     0000000000000000000000000000000000000000000000000000000000000001
	//1L 反码 1111111111111111111111111111111111111111111111111111111111111110
	//1L 补码 1111111111111111111111111111111111111111111111111111111111111111      补码 = 反码 + 1

	//这里是 -1L(64位1) 往左移动12位 111111111111111111111111111111111111111111111111 0000 0000 0000 0000
	// ~ 取反 					  000000000000000000000000000000000000000000000000 1111 1111 1111 1111
	//结果为                       2的12次方 - 1 = 4095
	@SuppressWarnings("FieldCanBeLocal")
	private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 4095

这里的mask就是,相当于最大值,我不能明白的是,为什么不直接写4095L 或者写2的12次方 - 1,这里的12次方的12 一样也可以写成这里的 SEQUENCE_BITS 为啥要搞这个位运算???,我查了一下,都没有这方面的问题,如果你懂的化,可以私信我,谢谢了

时钟回拨问题

我也是看别人说,会有这个时钟回拨问题,问题的出现在于,有可能运维人员手动的更改了服务器的时间,或者两个服务器时间不同,需要同步时间,就会导致这里的时钟回拨问题

解决方案:

第一种方案: 是如果是时间回拨只是一两次,并且时间跨度不是很大的化,例如1 到 3秒,那么就直接等,那么几秒,这样子相当于有冗余,但是影响也不是很大,但这种操作,不能再并发量很高的时候操作,不然肯定出问题

第二种方案: 就是美团 和百度的方案
这两的方案我就先不研究了,到时候我真的懂了,就来更新这里的博客,我估计我也看不懂

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

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

相关文章

Leecode之分割链表

一.题目及剖析 https://leetcode.cn/problems/partition-list-lcci/description/ 二.思路引入 就是将其分成大小两个链表,以x为分界线进行分堆,最后再将两链表合并 三.代码引入 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct Lis…

陪护系统|陪护小程序提升长者护理服务质量的关键

在如今逐渐老龄化的社会中&#xff0c;老年人对更好的护理服务需求不断增加。科技的进步使得陪护小程序系统源码成为提供优质服务的重要途径之一。本文将从运营角度探讨如何优化陪护小程序系统源码&#xff0c;提升长者护理服务的质量。 首先&#xff0c;我们需要对软件的设计和…

CAN通讯协议详解

阅读引言&#xff1a; 本篇博文想给需要的人介绍一下CAN总线&#xff0c; 这个也算是我从B站学习记得笔记分享吧也算是。简单的介绍了CAN总线的大致内容&#xff0c; 简述支持CAN功能的STM32的简单使用例程。本视频的中的图片内容均来自B站爱上半导体博主的内容。 CAN高质量教学…

AI嵌入式K210项目(28)-在线模型训练

文章目录 前言一、平台介绍二、创建项目三、上传数据集图像分类图像检测图片上传压缩包上传 四、新建任务总结 前言 前面我们使用已经训练好的模型在K210开发板上进行了人脸识别&#xff0c;口罩识别&#xff0c;手写数字识别等实验&#xff0c;那么模型除了使用已经训练好的&…

【Make编译控制 01】程序编译与执行

目录 一、编译原理概述 二、编译过程分析 三、编译动静态库 四、执行过程分析 一、编译原理概述 make&#xff1a; 一个GCC工具程序&#xff0c;它会读 makefile 脚本来确定程序中的哪个部分需要编译和连接&#xff0c;然后发布必要的命令。它读出的脚本&#xff08;叫做 …

机器学习2--逻辑回归(案列)

糖尿病数据线性回归预测 import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_diabetes diabetesload_diabetes() datadiabetes[data] targetdiabetes[target] feature_namesdiabetes[feature_names] data.shape df …

【数据结构】双向链表(链表实现+测试+原码)

前言 在双向链表之前&#xff0c;如果需要查看单链表来复习一下&#xff0c;链接在这里&#xff1a; http://t.csdnimg.cn/Ib5qS 1.双向链表 1.1 链表的分类 实际中链表的结构非常多样&#xff0c;以下情况组合起来就有8种链表结构&#xff1a; 1.1.1 单向或者双向 1.1.2 …

leetcode——滑动窗口题目汇总

本章总结一下滑动窗口的解题思路&#xff1a; 在字符串中使用双指针 left 和 right 围成的一个左闭右开的区域作为一个窗口。不断将 right 向右滑动&#xff0c;直到窗口中的字符串符合条件。此时将 left 向右滑动&#xff0c;直到窗口中的字符串不符合条件&#xff0c;期间需…

PyTorch 2.2 中文官方教程(三)

使用 PyTorch 构建模型 原文&#xff1a;pytorch.org/tutorials/beginner/introyt/modelsyt_tutorial.html 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 注意 点击这里下载完整示例代码 介绍 || 张量 || 自动微分 || 构建模型 || TensorBoard 支持 || 训练模型 ||…

大模型学习笔记二:prompt工程

文章目录 一、经典AI女友Prompt二、prompt怎么做&#xff1f;1&#xff09;注重格式&#xff1a;2&#xff09;prompt经典构成3&#xff09;简单prompt的python询问代码4&#xff09;python实现订阅手机流量套餐的NLU5&#xff09;优化一&#xff1a;加入垂直领域推荐6&#xf…

【glyphicon对照表】bootstrap样式可直接使用的图标大全

代码: <ul class="bs-glyphicons"><li><span class="glyphicon glyphicon-adjust"></span><span class="glyphicon-class">.glyphicon .glyphicon-adjust</span></li><li><span class=&qu…

为什么大模型需要向量数据库?

AIGC 时代万物都可以向量化&#xff0c;向量化是 LLM 大模型以及 Agent 应用的基础。 比如&#xff1a;爆火的 Google 大模型 Gemini 1.0 原生支持的多模态&#xff0c;在预训练的时候就是把文本、图片、音频、视频等多模态先进行 token 化&#xff0c;然后构建一维的“语言”…

《Python 网络爬虫简易速速上手小册》第7章:如何绕过反爬虫技术?(2024 最新版)

文章目录 7.1 识别和应对 CAPTCHA7.1.1 重点基础知识讲解7.1.2 重点案例&#xff1a;使用Tesseract OCR识别简单CAPTCHA7.1.3 拓展案例 1&#xff1a;使用深度学习模型识别复杂CAPTCHA7.1.4 拓展案例 2&#xff1a;集成第三方 CAPTCHA 解决服务 7.2 IP 轮换与代理的使用7.2.1 重…

【RabbitMQ(一)】:基本介绍 | 配置安装与快速入门

应该是新年前最后一篇博客了&#xff0c;明天浅浅休息一下&#xff0c;提前祝大家新年快乐捏&#xff01;&#x1f60a;&#x1f60a;&#x1f60a; 01. 基础理解 1.1 同步调用和异步调用 &#x1f449; 同步调用 的时候调用者会 阻塞 等待被调用函数或方法执行完成&#xff…

TELNET 远程终端协议

远程终端协议 TELNET TELNET 是一个简单的远程终端协议&#xff0c;也是互联网的正式标准。 用户用 TELNET 就可在其所在地通过 TCP 连接注册&#xff08;即登录&#xff09;到远地的另一个主机上&#xff08;使用主机名或 IP 地址&#xff09;。 TELNET 能将用户的击键传到…

ubuntu20.04 安装mysql(8.x)

安装mysql命令 sudo apt-get install mysql-server安装完毕后&#xff0c;立即初始化密码 sudo mysql -u root # 初次进入终端无需密码ALTER USER rootlocalhost IDENTIFIED WITH caching_sha2_password BY yourpasswd; # 设置本地root密码设置mysql远程登录 设置远程登录账…

【漏洞复现】EPON上行A8-C政企网关信息泄露漏洞

Nx01 产品简介 EPON上行A8-C政企网关是一款终端产品&#xff0c;提供企业网络解决方案。 Nx02 漏洞描述 EPON上行A8-C政企网关敏感信息泄露漏洞&#xff0c;攻击者通过敏感信息泄露获取管理员密码。 Nx03 产品主页 fofa-query: "ZXECS" && title"Web…

ZOJ 3537 Cake 【区间DP + 凸多边形三角剖分】

Cake 题意 给定平面坐标上的 n n n 个点&#xff0c;如果是凸多边形的话&#xff0c;就用最少的花费把这个多边形剖分成若干个三角形&#xff0c;剖分的线段端点只能是原多边形的顶点&#xff0c;一条线段的花费为&#xff1a; ∣ x i x j ∣ ∣ y i y j ∣ m o d p |x_i…

微信小程序开发学习笔记《16》uni-app框架

微信小程序开发学习笔记《16》uni-app框架 博主正在学习微信小程序开发&#xff0c;希望记录自己学习过程同时与广大网友共同学习讨论。建议仔细阅读uni-app对应官方文档 一、uni-app简介 **uni-app是一个使用Vue.js 开发所有前端应用的框架。**开发者编写一套代码&#xff…

70.SpringMVC怎么和AJAX相互调用的?

70.SpringMVC怎么和AJAX相互调用的&#xff1f; &#xff08;1&#xff09;加入Jackson.jar&#xff08;2&#xff09;在配置文件中配置json的消息转换器.(jackson不需要该配置HttpMessageConverter&#xff09; <!‐‐它就帮我们配置了默认json映射‐‐> <mvc:anno…