开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

揭秘 MySQL 的主从同步实现方案

发表于 2021-11-18

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

关于 MySQL 主从复制主要同步的是 binlog 日志,涉及到三个线程,一个运行在主节点(log dump thread),其余两个(I/O thread, SQL thread)运行在从节点,如下图所示:

1、如何实现主从一致

(1)主节点 binary log dump 线程

当从节点连接主节点时,主节点会创建一个 log dump 线程,用于发送 binlog 的内容。在读取 binlog 中的操作时,此线程会对主节点上的 binlog 加锁,当读取完成,在发送给从节点之前,锁会被释放。

(2)从节点 I/O 线程

当从节点上执行start slave命令之后,从节点会创建一个 I/O 线程用来连接主节点,请求主库中更新的 binlog。I/O 线程接收到主节点 binlog dump 进程发来的更新之后,保存在本地 relay-log(中继日志)中。

(3)从节点 SQL 线程

SQL 线程负责读取 relay log 中的内容,解析成具体的操作并执行,最终保证主从数据的一致性。

2、一主多从同步?

对于每一个主从连接,都需要三个进程来完成。当主节点有多个从节点时,主节点会为每一个当前连接的从节点建一个 binlog dump 进程,而每个从节点都有自己的 I/O 进程,SQL 进程。

从节点用两个线程将从主库拉取更新和执行分成独立的任务,这样在执行同步数据任务的时候,不会降低读操作的性能。比如,如果从节点没有运行,此时 I/O 进程可以很快从主节点获取更新,尽管 SQL 进程还没有执行。

如果在 SQL 进程执行之前从节点服务停止,至少 I/O 进程已经从主节点拉取到了最新的变更并且保存在本地 relay 日志中,当服务再次起来之后,就可以完成数据的同步。

3、主从复制的基本过程

(1)从节点上的 I/O 进程连接主节点,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;

(2)主节点接收到来自从节点的 I/O 请求后,通过负责复制的 I/O 进程根据请求信息读取指定日志指定位置之后的日志信息,返回给从节点。返回信息中除了日志所包含的信息之外,还包括本次返回的信息的 binlog file 的以及 binlog position;

(3)从节点的 I/O 进程接收到内容后,将接收到的日志内容更新到本机的 relay log 中,并将读取到的 binlog 文件名和位置保存到 master-info 文件中,以便在下一次读取的时候能够清楚的告诉 Master“我需要从某个 binlog 的哪个位置开始往后的日志内容,请发给我”;

(4)Slave 的 SQL 线程检测到 relay-log 中新增加了内容后,会将 relay-log 的内容解析成在主节点上实际执行过的操作,并在本数据库中执行。

4、MySQL 主从复制模式

MySQL 主从复制默认是异步的模式。MySQL 增删改操作会全部记录在 binlog 中,当 slave 节点连接 master 时,会主动从 master 处获取最新的 bin log 文件。并把 bin log 中的 sql relay。

(1)异步模式(mysql async-mode)

原理:客户端提交 COMMIT 之后主库,不需要等从库返回任何结果,而是直接将结果返回给客户端,这样做的好处是不会影响主库写的效率,但可能会存在主库宕机(就凉了),而 Binlog 还没有同步到从库的情况,也就是此时的主库和从库数据不一致。

这时候从从库中选择一个作为新主,那么新主则可能缺少原来主服务器中已提交的事务。所以,这种复制模式下的数据一致性是最弱的。

(2)半同步模式(mysql semi-sync)

原理:在客户端提交 COMMIT 之后不直接将结果返回给客户端,而是等待至少有一个从库接收到了 Binlog,并且写入到中继日志中,再返回给客户端。

这样做的好处就是提高了数据的一致性,当然相比于异步复制来说,至少多增加了一个网络连接的延迟,降低了主库写的效率。MySql5.7 支持设置应答从库的个数,保证 N 个从库同步完成后进行返回。

半同步模式不是 mysql 内置的,从 mysql 5.5 开始集成,需要 master 和 slave 安装插件开启半同步模式。

(3)全同步模式

全同步模式是指主节点和从节点全部执行了 commit 并确认才会向客户端返回成功。

  • END -

作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长。

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。

Thanks for reading!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

常常使用的分库分表

发表于 2021-11-18

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

背景

当遇到数据库表不断增大,一个接口查询的数据已经超过500ms了,就需要进行分库分表来保证查询效率了

问题

值得注意的是数据库表非常庞大时,而且主键ID不同库表时需要考虑主键ID唯一性问题。还有跨库join的问题

方案

目前分库分表有很多非常好的方案

  • 框架层:通过实现一些拦截器(比如Mybatis的Interceptor接口),增加一些自定义解析来控制数据的流向
  • 驱动层的包括:TDDL、ShardingJDBC
  • 代理层:MySQL Router、MyCat

下面用ShardingJDBC的代码实现来看下分库分表在项目中如何使用。

代码实现

引入依赖

首先需要引入ShardingJDBC的maven依赖

1
2
3
4
5
xml复制代码   <dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>1.5.4</version>
</dependency>

排除默认数据源

由于ShardingJDBC是在数据源上边做分库分表,所以排除原有的默认数据源

1
java复制代码@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})

分库规则设置

设置分库策略为键值大于30000走db0,小与等于30000走db1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Component
public class DbSharding implements SingleKeyDatabaseShardingAlgorithm<Integer> {

@Autowired
private Db0Config db0Config;

@Autowired
private Db1Config db1Config;

@Override
public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<Integer> shardingValue) {
Long value = shardingValue.getValue();
if (value > 30000) {
return db0Config.getDatabaseName();
} else {
return db1Config.getDatabaseName();
}
}

}

分表规则设置

设置分表规则,按奇数偶数分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Component
public class TbSharding implements SingleKeyTableShardingAlgorithm<Integer> {

@Override
public String doEqualSharding(final Collection<String> tableNames, final ShardingValue<Integer> shardingValue) {
for (String table : tableNames) {
if (table.endsWith(shardingValue % 2 + "")) {
return table;
}
}
throw new IllegalArgumentException();
}

}

分库分表策略配置

采用均匀分布模式,最终数据分布如下:

1
2
3
4
5
6
复制代码db0
├── user_0
└── user_1
db1
├── user_0
└── user_1

最关键的代码!设置数据源的分库分表策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码@Configuration
public class DataSourceConfig {

@Autowired
private Db0Config db0Config;

@Autowired
private Db1Config db1Config;

@Autowired
private DbSharding dbSharding;

@Autowired
private TbSharding tbSharding;

@Bean
public DataSource getDataSource() throws SQLException {
return buildDataSource();
}

private DataSource buildDataSource() throws SQLException {

Map<String, DataSource> dataSourceMap = new HashMap<>(2);
// 添加数据源 db0和db1
dataSourceMap.put(db0Config.getDatabaseName(), db0Config.dataSource());
dataSourceMap.put(db1Config.getDatabaseName(), db1Config.dataSource());
// 设置db0为默认源
DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, db0Config.getDatabaseName());

// 设置分表,将查询虚拟表user根据规则映射到真实表中去
TableRule userTableRule = TableRule.builder("user")
.actualTables(Arrays.asList("user_0", "user_1"))
.dataSourceRule(dataSourceRule)
.build();

// 分库分表策略
ShardingRule shardingRule = ShardingRule.builder()
.dataSourceRule(dataSourceRule)
.tableRules(Arrays.asList(userTableRule))
.databaseShardingStrategy(new DatabaseShardingStrategy("user_id", dbSharding))
.tableShardingStrategy(new TableShardingStrategy("id", tbSharding)).build();
DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);
return dataSource;
}


@Bean
public KeyGenerator keyGenerator() {
return new DefaultKeyGenerator();
}

}

概念解释

TableRule

表规则配置对象,内嵌 TableRuleBuilder 对象进行创建。

数据单元

  • DataNode
    静态分库分表数据单元
    数据分片的最小单元,由数据源名称和数据表组成。
    例:ds_1.t_order_0。配置时默认各个分片数据库的表结构均相同,直接配置逻辑表和真实表对应关系即可。
  • DynamicDataNode
    动态表的分库分表数据单元
    逻辑表和真实表不一定需要在配置规则中静态配置。比如按照日期分片的场景,真实表的名称随着时间的推移会产生变化

TableRuleBuilder

TableRuleBuilder 调用 #build() 方法创建 TableRule

分库/分表策略

  • databaseShardingStrategy :分库策略
  • tableShardingStrategy :分表策略

ShardingRule

分库分表规则配置对象,内嵌 ShardingRuleBuilder 对象进行创建。

dataSourceRule

dataSourceRule,数据源配置对象。ShardingRule需要数据源配置正确。这点和 TableRule 是不同的。TableRule 对 dataSourceRule只使用数据源名字,最终执行SQL 使用数据源名字从 ShardingRule 获取数据源连接

主键生成

  • generateKeyColumn :主键字段
  • keyGenerator :主键生成器

小结

本文简单的解释了为什么使用分库分表,以及分库分表的方案,以ShardingJDBC作为示例给出了一部分代码实现,并解释了其中的概念,文章中的部分内容参考各位大佬对分库分表的理解做出整理。

参考

  • “分库分表” ?选型和流程要慎重,否则会失控
  • Sharding-JDBC 源码分析
  • 《SpringBoot使用Sharding-JDBC分库分表》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NIO网络编程(九)—— 再探NIO、BIO概念

发表于 2021-11-18

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

前面在Netty编程(一)—— BIO和NIO - 掘金 (juejin.cn)中介绍了BIO、伪异步IO以及NIO的基本概念,之后的七篇博客介绍了NIO编程的相关知识,这篇博客是NIO网络编程的结尾,讲解一下关于NIO和BIO的概念知识。

Stream与Channel

  • stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)
  • stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用
  • 二者均为全双工,即读写可以同时进行

IO模型

阻塞IO

在这里插入图片描述

用户线程进行read操作时,需要等待操作系统执行实际的read操作,首先要等待数据通过网络到达,然后会复制数据,此期间用户线程是被阻塞的,无法执行其他操作。

非阻塞IO

在这里插入图片描述

  • 用户线程

在一个循环中一直调用read方法,若内核空间中还没有数据可读,立即返回。但是对于非阻塞IO来说, 只是在等待阶段非阻塞,在数据复制阶段还是被阻塞住。

  • 用户线程发现内核空间中有数据后,等待内核空间执行复制数据,待复制结束后返回结果

多路复用

在这里插入图片描述

在之前的Java中通过Selector实现多路复用(具体可以看《Netty编程(六)—— nio.Selector之基本使用 - 掘金 (juejin.cn)》 以及 《Netty编程(七)—— nio.Selector之读写事件 - 掘金》 (juejin.cn))

  • 当没有事件是,调用select方法会被阻塞住
  • 一旦有一个或多个事件发生后,就会去处理对应的事件,从而实现多路复用

多路复用与阻塞IO的区别

这里说一下多路复用与阻塞IO的区别

  • 阻塞IO模式下,若线程因accept事件被阻塞,发生read事件后,仍需等待accept事件执行完成后,才能去处理read事件,这也正是阻塞IO的最大问题
  • 多路复用模式下,一个事件发生后,若另一个事件处于阻塞状态,不会影响该事件的执行

异步IO

在这里插入图片描述

所谓异步IO就是线程1调用方法后理解返回,不会被阻塞也不需要立即获取结果当方法的运行结果出来以后,由线程2将结果返回给线程1。

AIO

AIO 是上面讲到的异步IO,他是用来解决数据复制阶段的阻塞问题

  • 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
  • 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果

这里可以举一个异步读取文件内容的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Slf4j
public class TestAIO {
public static void main(String[] args) throws IOException {
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("data2.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(16);
log.debug("read begin...");
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override //read成功
public void completed(Integer result, ByteBuffer attachment) {
log.debug("read complete...{}",result);
attachment.flip();
ByteBufferUtil.debugAll(buffer);
}
@Override //read失败
public void failed(Throwable exc, ByteBuffer attachment) {

}
});
log.debug("read end...");
System.in.read();
}
}

结果如下,可以看见读取并打印结果确实是异步执行的

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud Alibaba 学习 -- 2、R

发表于 2021-11-18

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

学习视频(B站):www.bilibili.com/video/BV1Mt…

GitHub 源码地址:github.com/tyronczt/sp…

简介:

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。

整合ribbon

由于在consumer的pom中已经引入 spring-cloud-starter-alibaba-nacos-discovery , 它已经引入ribbon:

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>

所以只需要在 restTemplate 的 bean 中添加 @LoadBalanced 注解,即可以使用ribbon

1
2
3
4
5
6
7
8
9
10
java复制代码@Configuration
public class ConsumerConfig {

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}

}

ConsumerController 调用接口,默认采用 轮询 方式

1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class ConsumerController {

@Autowired
private RestTemplate restTemplate;

@GetMapping("/index")
public String index() {
return "consumer远程调用provier:" + this.restTemplate.getForObject("http://provider/index", String.class);
}
}

设置调用方式为 随机【只需在yml配置文件中添加已经定好的规则即可】:

1
2
3
yml复制代码provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

设置调用方式为 Nacos 权重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {

@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;

@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
//读取配置文件
}

@Override
public Server choose(Object o) {
ILoadBalancer loadBalancer = this.getLoadBalancer();
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) loadBalancer;
//获取要请求的微服务名称
String name = baseLoadBalancer.getName();
//获取服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
try {
Instance instance = namingService.selectOneHealthyInstance(name);
log.info("选择的实例是port={},instance={}",instance.getPort(),instance);
return new NacosServer(instance);
} catch (NacosException e) {
e.printStackTrace();
return null;
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

逆向系列-你一定要懂的MD5加密 逆向的步骤 常见的加密方法

发表于 2021-11-18

逆向的步骤

逆向的步骤主要包含以下几点:

  • 抓包

抓包的过程其实很简单,在学爬虫入门的时候,想必这是每一个同学都必学的一个阶段。打开开发者调试工具,刷新页面,即可在network面板查看到加载出来的数据包。

  • 调试

当找到目标数据包时,根据目标数据包form data中的关键字参数进行全局查找,便基本上可以查找到目标参数所在的js文件,通过断点的方式即可查找到目标值。注意:这仅仅只是针对大多数网站的方式。

  • 扣取js

当找到目标参数的值的生成方式时,便需要通过方法栈的跟踪找到目标参数生成值的方法即可,并将js代码复制出来并运行代码即可。

  • 改写js

有时候,扣去下来的js代码存在不规范,需要进行简单的修改。这一点需要各位同学熟悉js的代码。

  • 本地运行代码

当做完上述的步骤之后,便可以将代码复制出来在本地运行出结果与已知结果相同即可。

常见的加密方法

MD系列

MD系列中有三种加密方式:MD5、MD4、MD2。

关于MD系列中重点讲解的内容就是MD5,不论是多长的字符串均能加密成32位或16位字符。MD5编码是由数字与字母组成,因此看到位数以及字母组成形式的字符可以迅速判断可能是MD5编码。

MD5编码具有不可逆的特点,明文可以被加密成密文,但是无法根据密文以及加密方式反推明文,极大的保证了安全性。但是目前有不少在线解密MD5的网站,它是怎么样做的呢?

其实很简单,它将常见的明文加密成MD5密文的形式,将所得结果保存起来,当需要查询的时候进行匹配即可。其实这种方法就类似于暴力破解。

个人经验之谈

关于MD5的加密,我说一下个人的经验一般来说它会在账号与密码登陆的时候动手脚。(对于一部分网站)

我们在输入密码的时候会采用123456,方便我们记忆。

为什么这样说呢?

因为123456所对应的16位和32位MD5字符串分别是:

1
2
bash复制代码49ba59abbe56e057	#16位
e10adc3949ba59abbe56e057f20f883e # 32位

这一点我们需要熟记,123456的16位密文是49开头,而32位123456的密文是e10开头。当在数据包中发现此类信息是即可判断。

另一点就是程序员一般都不会没事找事干。加密函数与加密参数都是放在同一个js文件中,如果实在是找不到也没有关系,可以考虑采用方法栈的跟踪进行查找。

今日网站

aHR0cHM6Ly93d3cuc29odS5jb20v

本次网站主要是考虑是如何破解登陆上的MD5加密,和开始所写的内容相同,一定要按照逆向的步骤进行操作。

首先先进行抓包操作:

如上图所示,我输入的密码是123456,它也给我通过,只是点击登陆的时候会显示密码或账号错误。有些网站是不会通过123456这种密码的,那么如果不通过的话,就要考虑换一个复杂一点的即可。

大家来看一下,这是不是就是我们要查找的数据包呀?

很明显,当我们看到password的值时是否就能联想到刚刚所描述的加密方式的特点呢?相信各位同学都已经心中有数了吧!

form data里面的参数都是我们可以进行搜索的内容,比如说:userid、password、persistentCookie等等,或者是最上方的url里面的值也有可能是我们要进行全局搜索的内容哦。

为了节省时间,我们就直接对password进行全局搜索。

经过搜索之后,我们从图中可以发现,出现的结果还是蛮多的。但是大家有没有想过一个问题,那就是MD5加密的值是怎么样传递给password的呢?一般来说是不是对象.password = xxx这样的形式来传递的呢?因此搜索方式可以修改为password=:

在password的后面我没有加上空格,因为现在所搜索的内容是没有经过格式化的,如果格式化代码之后,那么在password的后面就必须要加上空格了。(这一点要注意)

从上图可以看出,所搜结果又被过滤掉了很多,只剩下其中一个了,这正合我意。

当进入这个js文件之后,再将password搜索一遍,可以看到password的结果有4个。

接下来便在此行打上断点,来跟踪加密函数。从上面的图中可以发现它是通过utils对象生成的加密参数。

打上断点之后,可以发现这个确实是我们需要查找的内容,经过上面的分析,接下来便要查找utils关键字。

经过搜索发现,在当前的js代码中确实是存在utils关键字,看上去也是一个对象,那这就刚刚好验证了我们刚才所描述的内容了。

在这里也分享一下我扣取js代码的经验:

可以先将JS代码全部复制下来到本地,因为在浏览器中括号无法将成对展现,但是在本地文件中是可以成对出现,方便扣取的时候,不会少了括号或者是代码而报错。

将utils代码扣去下来之后,便将其进行运行出结果即可。

至此,关于MD5的加密逆向就讲解到这里了。

更多精彩内容,期待下次再见!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hash相关基本内容(构造函数 冲突解决)

发表于 2021-11-18

这是我参与18月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

1.Hash表(散列表)
根据关键码值而直接进行访问的数据结构(,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度)我们可以看之前分享的查找算法主要都是通过比较来找位置,而hash在这个基础上做出了一些改变 他并没有去比较 而是通过映射函数去定位查找记录 而这个映射函数就是散列函数

2.我们在知道HASH表和HASH函数之后 我们就要进一步的思考 我们怎样来建立HASH函数呢?每一个方法有什么优劣

  • 直接定址法
    可以直接通过一个线性函数就可以直接定位到关键字存放的散列表的位置(y = ax + b 其中x为关键字)
    这种直接定址法就是很简单 例如我存放个年级的人数(一年级=1,二年级=2 ….) 我用y=x 就可以建立一个映射函数一一映射,但直接地址法对这种连续且数据量较小的数据比较好 但存储的关键字之间关联性不大 且不连续 用这个来存储值就会浪费很多空间 则并不适用
  • 除留去余法
    这个方法应该经常用到:选择一个合适的整数P 对关键之进行除留取余来计算散列位置 H(K)=K mod p;其中p的选取是关键 因为p选的好 产生冲突的几率会很小 则查找效率就会很高 反之就很低,p最好取取不大于表长且最接近表长m素数时效果最好(素数:除了1和它本身以外不再有其他因数的自然数)
  • 平方取中法

看个例子就明白了:关键字序列是{45,34,54}平方后是{2025,1156,2916}则散列后的地址{02,15,91}

数字分析法 折叠法 随机数法这些 想要了解可以进一步网上搜索

3.在我们利用hash函数存储时出现了可能会出现冲突 下面就是冲突的解决办法

  • 线性探测法
    很容易理解 发生冲突直接从下一个位置寻找散列地址 依次类推 知道找到空的散列地址就存入

H=(H(key)+d)%m,(d=1,2,3…,m-1)其中m为表长

  • 二次探测法
  • H=(H(key)+d)%m*,(d=1^2,-1^2,2^2,-2^2,…..,q^2,-q^2,q<=根号下m) 其实时线性探测的进一步
  • 随机探测法

d为{1,2,3,…,m-1}中够成一个随机数列且从中顺序取的一个数

  • 再散列函数法
    则需要构造不同得HASH函数,Hi=RH1(key) i=1,2,…,k,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到不发生冲突
  • 链地址法
    当冲突发生时 前面得冲突解决办法时移动位置去存放 而这个链地址法当产生冲突并没有去换地址,而是通过链把具有相同位置得记录链起来

例如下图显示:

image.png
( 图片来源网上)

JAVA中得HASH结构:
HashMap:
在这个得基础上,我想分享以下在hashMap中对HASH得简单使用。
在HashMap中我们存值一般时

1
2
arduino复制代码HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("test", "test");

那来看以下hashMap的Put:

1
2
3
4
5
6
7
vbnet复制代码public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

注意其中的hash(key) ,其中hashCode也是Obejct中的一个方法 我们可以重写他 而hashCode则是我们所说的散列函数 他需要计算返回hash码,而在Object中hashCode方法他实际上返回的值时实例对象的内存地址(当然你可以重写他)因为实例对象的内存地址是唯一的 他们产生冲突的几率也不大。
image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP实现NBA赛事结果即时推送 原理 使用的服务 消息效果

发表于 2021-11-18

对于平时不能看NBA直播,但又希望第一时间知道喜欢的湖人队的比赛结果。动动小手,简单实现监测湖人队的比赛结果,发送到自己微信、邮箱、钉钉上面。

原理

采用PHP发起请求聚合数据提供的NBA赛事API,通过解析接口返回到JSON,提取我关注的湖人队比赛结果,并将结果信息通过聚合云推服务推送至我需要接收的终端。

使用的服务

NBA赛事查询接口: www.juhe.cn/docs/api/id…

消息推送服务: tui.juhe.cn

消息效果

如果监测到关注的比赛结果,你在聚合云推配置的接收终端将会收到通知,类似如下:

钉钉机器人:

image.png

微信公众号:

image.png

邮箱:

image.png

PHP代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
php复制代码<?php
/**
* PHP实现NBA赛事结果通知即时推送
*/
//设置时区
if (PHP_VERSION >= '5.1') date_default_timezone_set('PRC');

$nba = new Nba();
$nba->run();

class Nba
{
// 关注的球队列表
// private $teams = ['洛杉矶湖人', '布鲁克林篮网'];
private $teams = ['洛杉矶湖人'];

// 开始执行
public function run()
{
echo "开始监测NBA今日赛程 - ". date("Y-m-d H:i:s") . PHP_EOL;

// 查询今日NBA比赛清单
$todayMatchsResult = $this->getMatchResult();
if ($todayMatchsResult['code'] == '1') {
// 解析比赛结果,提取需要推送的比赛结果
$parseMatchsResult = $this->parseMatchs($todayMatchsResult['matchs']);
if ($parseMatchsResult) {
$this->pushMsg($parseMatchsResult);
} else {
echo "暂无比赛结果需要推送";
}
} else {
// 查询失败
echo $todayMatchsResult['msg'];
}
}

/**
* 将需要推送的比赛结果, 通过 聚合云推 推送至邮件、钉钉、微信等
* 聚合云推官网: https://tui.juhe.cn/?f=nba
* @param null $msgs
*/
private function pushMsg($msgs = null)
{
// 记录已经推送过的msg文件
$logFile = dirname(__FILE__).DIRECTORY_SEPARATOR."notify_log.txt";
$logContent = file_get_contents($logFile);
$logList = explode("\n", $logContent);

// 在聚合云推个人中心获取到的token
$token = "cd513bd*******b7261e566a3";
// 在聚合云推创建的ServiceID
$service_id = "1O***Ho";
// 自定义标题
$title = "NBA赛事结果";
$doc_type = "text";

foreach ($msgs as $m) {
// 消息内容MD5,简单记录本地文件,用于判断是否已经推送,避免重复 (当然也可以使用数据库、缓存等手段)
$mHash = md5($m);
if (in_array($mHash, $logList)) {
// 重复推送
echo "{$m},推送结果:999 - 已经推送过,无需重复推送" . PHP_EOL;
} else {
// 开始推送
$params = [
'token' => $token,
'service_id' => $service_id,
'title' => $title,
'doc_type' => $doc_type,
'content' => $m
];
$pushResContent = $this->juheHttpRequest('https://tui.juhe.cn/api/plus/pushApi', http_build_query($params), 1);
$pushRes = json_decode($pushResContent, true);
echo "{$m},推送结果:{$pushRes['code']} - {$pushRes['reason']}" . PHP_EOL;

// 记录log
file_put_contents($logFile, $mHash, FILE_APPEND);
}
}
}

/**
* 解析NBA比赛结果,提取关注的球队比赛结果
* @param null $matchs
*/
private function parseMatchs($matchs = null)
{
$matchText = null;
if ($matchs) {
foreach ($matchs as $m) {
if (in_array($m['team1'], $this->teams) || in_array($m['team2'], $this->teams)) {
// 有关注的球队且完赛
if ($m['status'] == '3') {
// 整合比赛球队和结果信息
$matchText[] = "{$m['team1']} {$m['team1_score']} - {$m['team2_score']} {$m['team2']}";
}
}
}
}
return $matchText;
}

/**
* 查询NBA今日比赛状态
* 通过聚合提供的接口,接口详情:https://www.juhe.cn/docs/api/id/92?f=nba
*/
private function getMatchResult()
{
// NBA赛事接口地址
$apiUrl = 'http://apis.juhe.cn/fapig/nba/query';
// 接口地址请求Key, 自行替换
$apiKey = '69e76b5*********4b87690c8';
$params = [
'key' => $apiKey
];
$requestContent = $this->juheHttpRequest($apiUrl, http_build_query($params));
$requestResult = json_decode($requestContent, true);
if ($requestResult) {
$errorCode = $requestResult['error_code'];
if ($errorCode == 0) {
// 请求成功
$matchs = isset($requestResult['result']['matchs']) ? $requestResult['result']['matchs'] : null;
if ($matchs) {
$todayMatchs = null;
foreach ($matchs as $key => $match) {
$matchDate = $match['date'];
if ($matchDate == date("Y-m-d")) {
// 今日比赛列表
$todayMatchs = $match['list'];
break;
}
}
// 返回比赛列表
return ['code' => '1', 'msg' => '请求成功', 'matchs' => $todayMatchs];
} else {
// 无比赛记录
return ['code' => '1', 'msg' => '请求成功', 'matchs' => null];
}
} else {
// 请求异常
return ['code' => '2', 'msg' => '请求异常:' . $errorCode, 'matchs' => null];
}
} else {
// 请求异常,可能网络异常
return ['code' => '2', 'msg' => '请求异常:网络异常', 'matchs' => null];
}
}

/**
* 发起网络请求函数
* @param string $url 请求的URL
* @param bool $params 请求的参数内容
* @param int $ispost 是否POST请求
* @return bool|string 返回内容
*/
private function juheHttpRequest($url, $params = false, $ispost = 0)
{
// $httpInfo = [];
$ch = curl_init();

curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 12);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($ispost) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt($ch, CURLOPT_URL, $url);
} else {
if ($params) {
curl_setopt($ch, CURLOPT_URL, $url . '?' . $params);
} else {
curl_setopt($ch, CURLOPT_URL, $url);
}
}
$response = curl_exec($ch);
if ($response === FALSE) {
// echo "cURL Error: ".curl_error($ch);
return false;
}
// $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// $httpInfo = array_merge($httpInfo, curl_getinfo($ch));
curl_close($ch);
return $response;
}
}

计划任务配置

您可以通过crontab等方式定时(每分钟)去执行代码监测。
比如crontab配置方式:
命令行输入crontab -e进入计划任务配置

1
2
javascript复制代码# 每分钟执行一次
*/1 * * * * /usr/bin/php nba.php >> /data/log.txt
1
bash复制代码cat /data/log.txt
1
2
3
4
5
6
7
8
9
yaml复制代码洛杉矶湖人 102 - 109 密尔沃基雄鹿,推送结果:200 - success
开始监测NBA今日赛程 - 2021-11-18 18:41:00
洛杉矶湖人 102 - 109 密尔沃基雄鹿,推送结果:999 - 已经推送过,无需重复推送
开始监测NBA今日赛程 - 2021-11-18 18:42:01
洛杉矶湖人 102 - 109 密尔沃基雄鹿,推送结果:999 - 已经推送过,无需重复推送
开始监测NBA今日赛程 - 2021-11-18 18:42:39
洛杉矶湖人 102 - 109 密尔沃基雄鹿,推送结果:999 - 已经推送过,无需重复推送
开始监测NBA今日赛程 - 2021-11-18 18:43:00
洛杉矶湖人 102 - 109 密尔沃基雄鹿,推送结果:999 - 已经推送过,无需重复推送

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kafka 中的消费者组

发表于 2021-11-18

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

消费者组是什么

消费者组(Consumer Group)是 Kafka 的重要特性之一,简而言之,消费者组就是让若干个消费者实例(Consumer Instance)形成一个消费者组,共同消费共同消费同一个或者多个 Topic 的不同 Partition 中的消息,注意,每一个 Partition 只能由消费者组中的一个 Consumer 来消费。

多个消费者组之间是相互独立的,它们可以订阅同样的主题进行消费。

消费者组如何工作

当一个消费者组订阅了一个或者多个 Topic 的时候,这些 Topic 中所有的 Partition 会被「分配」给消费者组中的消费者实例,这些消费者实例各自处理自己分配到的分区的消息。

前面介绍过,一个分区只能分配给一个消费者实例,不能被多个实例同时消费,同时,一个实例可能被分配多个分区。

举一个具体的例子。假设一个消费者组订阅了 T1、T2、T3 三个 Topic,他们中分别有3个、5个、10个分区,加起来一共18个分区。此时:

  • 如果消费者组中正好有18个实例,那么,每个实例负责消费一个分区的消息。
  • 如果消费者组中的实例数超过了总的分区数,比如有20个实例,那么将会有2个实例是空闲的,什么都不做,除非有其他实例挂掉触发 Rebalance。
  • 如果消费者组中有 6 个实例,那么每个实例会被分配3个分区。

因此,推荐消费者组中的实例数与订阅的 Topic 的总分区数相同。

Rebalance

上一部分提到了 Rebalance 机制,它是 Kafka 中将消息分区分配给消费者组中的消费者实例的机制。上面的例子中,将 18 个分区分配给 6 个消费者实例的过程,就是 Rebalance。除了在消费者组订阅 Topic 后需要进行这样的分配,还有一些情况会触发 Relalance:

  1. 消费者组中的实例发生变化的时候,包括但不限于个别消费者实例宕机、有新的实例加入、有实例被踢出等等情况。此时,因为有些分区对应的消费者实例不在组中了,或者有新的实例可以「分担」其他实例的工作,那么,此时会触发 Rebalance。
  2. 订阅的主题发生变更的时候。因为消费者组可以通过匹配表达式来订阅主题,比如,订阅所有以 abc 开头的主题。当有新的主题被创建,如果这个主题的名称是以 abc 开头的,那么,消费者组会自动消费这个主题,此时,就需要重新分配分区和消费者实例的关系。
  3. 主题的分区发生变化的时候,无论分区数增多还是减少,都需要重新分配消费者实例,以达到均衡。

当以上因素出现的时候,Rebalance 机制能都实现自动并且尽可能均衡的分配,但是,目前它还有一些问题。

第一,当执行 Relalance 的时候,消费者组中所有消费者实例都会停止对消息的消费,知道 Rebalance 工作完成。这类似于垃圾回收器中经常会提到了 stop-the-world,因此,要尽可能避免频繁地 Rebalance。

第二,目前的 Rebalance 操作十分「不智能」,每次 Rebalance 操作都会将所有的分区进行重新分配,而不是采取尽量减少变动的方式,这一问题导致在分区数和消费者实例数比较多的情况下, Rebalance 操作是一个耗时的操作,因此,再次建议,要尽可能避免频繁地 Rebalance。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mysql 温故知新系列「字符串函数(trim, find_

发表于 2021-11-18

「这是我参与11月更文挑战的第 18 天,活动详情查看:2021最后一次更文挑战」

trim

常规用于删除数据的前后空格,使用的方式为 trim(str)

如果需要删除前后指定的字符串,其格式为 trim(remstr FROM str)

image.png

此外 trim 还提供一个参数来限定删除 【前+后|前|后】 的指定字符串

image.png

ltrim/rtrim

如果我们的需求仅仅是删除字符串前面(或者后面)的空格,可以使用 mysql 中对 trim 封装的简化函数 ltrim(str), rtrim(str)

1
2
sql复制代码trim(leading ' ' from str) ==> ltrim(str)
trim(trailing ' ' from str) ==> rtrim(str)

find_in_set

1
sql复制代码find_in_set(str, strList)

当字符串是以 , 分割的数据列表,即这个字符串可以根据 , 切割得到一个列表数据,在这种情况下,可以使用 find_in_set 查询一个子字符串是否在那个列表中,在则返回数字下标(从 1 开始)如果没有找到,则返回 0

image.png

image.png

format

通常,我们在查询中使用了聚合函数 avg 去计算平均值,很有可能得到一个小数,有的时候要求这个值需要保留几位小数,以及将数按千分位隔开

1
sql复制代码format(N, D, local)

N 表示需要处理的值,D 表示保留几位小数,local 表示语音区域,默认为 en_US,即数格式化后的效果为 xx,xxx,xxx.xx

image.png

lcase/ucase

mysql 中可以提供了对字符串进行大小写转化的函数:

  • lcase 字符串转小写
  • ucase 字符串转大写
1
2
3
4
sql复制代码SELECT lcase('AAA');
--> aaa
SELECT ucase('aaa');
--> AAA

repeat

1
sql复制代码SELECT repeat(str, n)

将字符串 str 重复 n 次后返回

image.png

lpad/rpad

在字符串的左边或右边填充指定字符,使这个字符串的长度达到指定的长度

最常见的场景,我们需要生成定长的流水号,比如,流水号格式限定5位,位数不够用 0 填充。

一种偷懒的方案就是,我最高位直接从 1 开始,这样就不会存在开头位数不够的情况

另一种方法就是使用 mysql 提供的 lpad

1
2
sql复制代码lpad(str, length, repeatStr);
rpad(str, length, repeatStr);

str 需要处理的字符,length 表示处理后的字符长度,repeatStr 表示用来填充的字符


现在来一个角度刁钻的,比如我给 1 用 abc 填充到 5 位,这又是怎么填充的呢?

image.png

如测试所示,直接循环使用字符串 abc 进行填充,填充足够的位数后就终止

原创文章,未经允许,禁止转载

– by 安逸的咸鱼

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python * ** 打包解包详解 可用位置

发表于 2021-11-18

参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

里边出现的英文都来自于python官网的开发者指南PEP 448 – Additional Unpacking Generalizations | Python.org

本文主要就是介绍*和**对此展开详细介绍。

Unpacking is proposed to be allowed inside tuple, list, set, and dictionary displays.

解包操作可以应用于元组、列表、集合、字典。

  • *args:argument用于列表、元组、集合
  • **kwargs:kwargument用于字典

首先先回顾一下range,以及用range生成元组、列表和集合。

1
2
3
4
5
6
py复制代码li = list(range(7))
print(li)
tu = tuple(range(7))
print(tu)
se = set(range(7))
print(se)
1
2
3
scss复制代码[0, 1, 2, 3, 4, 5, 6]
(0, 1, 2, 3, 4, 5, 6)
{0, 1, 2, 3, 4, 5, 6}

可用位置

*和**可以用于任意位置的解包。

Specifically, in function calls, in comprehensions and generator expressions, and in displays.

  • function calls:函数调用
  • comprehensions and generator expressions:生成器推导式
  • displays:显示(我的理解是直接输出?)

display

所谓的用于显示我觉得就是直接输出,(总不可能是直接输出😶)。

1
2
3
4
5
py复制代码li = list(range(7))
tu = tuple(range(7))
se = set(range(7))

print(*li,*tu,*se)
1
复制代码0 1 2 3 4 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6

上边是*直接完成了列表、元组和集合的解包。

但是不能用**对字典解包,print(**{"name":"sian","name":25})会报错SyntaxError: invalid syntax。

虽然不能用于直接输出,但是可以切片。

1
2
3
4
py复制代码dic = {"name":"sian","name2":"sian"}
a = {'a':0,**dic,'c':1}

print(a)
1
css复制代码{'a':0,"name":"sian","name2":"sian",'c':1}

**都可以切片了,那*也可以:

1
2
3
4
py复制代码li = [1,2,3]
a = [1,*li,0]

print(a)
1
csharp复制代码[1, 1, 2, 3, 0]

Comprehensions and generator expressions

生成器推导式就是:

1
py复制代码print(i for i in range(7))
1
csharp复制代码<generator object <genexpr> at 0x000002502D43A510>

输出告诉你这是一个生成器对象。

可以用生成器对象创建列表和集合,但是直接无法创建元组。

1
2
3
py复制代码li = [i for i in range(7)]
se = {i for i in range(7)}
print(li,se)
1
csharp复制代码[0, 1, 2, 3, 4, 5, 6] {0, 1, 2, 3, 4, 5, 6}

用*解包生成器对象就是:

1
py复制代码print(*[i for i in range(7)])
1
复制代码0 1 2 3 4 5 6

直接连变量名都省了:clap:。

Function call

用于函数调用就很常见了。

1
2
3
4
5
6
7
py复制代码def function(num,*args):
print(num)
print(args)
print(*args)

function(1,2,3,4,5,6,7)
function('a',1,2,3,'b')
1
2
3
4
5
6
css复制代码1
(2, 3, 4, 5, 6, 7)
2 3 4 5 6 7
a
(1, 2, 3, 'b')
1 2 3 b

*args用于接收任意长度的参数并将其打包存储在元组中。

但是位置不够灵活,它会接收从*args开始位置到最后的所有参数。

  • 比如def function(first,*args,last):,last会接收不到参数,报错function() missing 1 required keyword-only argument: 'last'

但是你可以手动指定参数来解决这个问题。比如这样强行给last一个值。

1
2
3
4
5
6
python复制代码def function(first,*args,last="c"):
print("first:",first)
print(args)
print("last:",last)

function(1,2,3,4,5,6,7)
1
2
3
makefile复制代码first: 1
(2, 3, 4, 5, 6, 7)
last: c
  • *args之前的参数无法手动指定。
1
2
3
4
5
py复制代码def function(first='a',*args):
print("first:",first)
print(args)

function(1,2,3,4,5,6,7)
1
2
makefile复制代码first: 1
(2, 3, 4, 5, 6, 7)

再看一下**接收参数将其打包为字典。

1
2
3
4
5
py复制代码def function(first='a',**kwargs):
print("first:",first)
print(kwargs)

function(1,name = "Sian", age = 25,b=2)
1
2
css复制代码first: 1
{'name': 'Sian', 'age': 25, 'b': 2}

参数列表的位置有一点和*一样,也是之后不能有参数。会报错SyntaxError: invalid syntax。但是前面的参数可以指定了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…290291292…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%