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

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


  • 首页

  • 归档

  • 搜索

如何用 redis 实现批量 pop? 1简单的循环 po

发表于 2021-11-18

最近在业务上遇到需要批量pop的场景,起初以为是个很简单的方案,然而在实现过程中走了很多弯路,和大家分享一下,避免踩坑

1.简单的循环 pop

这是最简单的方案,伪代码如下所示:

1
2
3
4
css复制代码var values []string
for(i:=0;i<num;i++){
values=append(values,redis.rpop())
}

这也是我们最初的方案,后来随着业务量上来后,大量的网络开销导致耗时严重,业务逐渐不能接受

总结

优点

  • 最简单的方案

缺点

  • 串行,耗时严重

2.易实现的并发 pop

通过并发 pop,进而缩短网络耗时,伪代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码var values []string
locker:=new(sync.mutux)
wg:=new(sync.WaitGroup)
for(i:=0;i<n;i++){
 wg.Add(1)
go func(){
locker.Lock()
values=append(values,redis.rpop())
locker.Unlock()
   wg.Done()
}
}
wg.Wait()

最终耗时≈网络中最慢的一次请求

总结

优点

  • 最容易想到的优化方案
  • 性能提升显著

缺点

  • 需要处理并发
  • 有大量的网络开销,server端处理网络连接的压力较大

3.lrange+ltrim+txpipeline

开始有同事提到用 lrange 来批量获取队列中的消息,但后来发现 lrange 只能查看,而不能同时移除消息,这就令我们很头疼了

上网上查了下,说是接着用 ltrim,同时用 txpipelline 来保证原子性,如下所示:

1
2
3
4
5
6
css复制代码p:=redis.txpipeline()
// 查询 0~n-1 之间的元素
p.lrange(key, 0, n - 1)
// 保留 n~最后(-1)之间的元素,换言之删除 0~n-1 之间的元素
p.ltrim(key, n, -1)
values:=p.execute()

一眼看上去确实很完美,一个打包命令实现了批量pop,避免了大量网络开销,但是真的是这样吗?

txpipeline的原子性

pipeline

首先,pipeline本身非原子性,其只是将一批命令一口气发到server,server依次处理这些命令,中间可能穿插执行其他 client 传来的命令,所以只用 pipeline的话是无法保证pop正确运行的,存在pop同一个元素的风险,或者多删元素等并发风险

transaction

其次,我们来了解下redis的事务,其主要由multi 和exec构成,用来封装一个原子执行单位,其实本质上,不算是真正的事务:

  1. 不能回滚
  2. 部分命令出错后,仍然能继续执行

该事务能显性检查一些语法错误,从而避免执行事务;但对于一些单条语法上无法辨别的错误是无法check的,例如对set进行pop,这就导致了上述 2 的发送

txpipeline

所以 txpipeline 就是在 pipeline前后加入了事务操作,从而保证了这批命令的原子性。从这个角度上看上述实现是没有问题的,但由于事务开销比较大,会阻塞其他命令,导致server响应较慢

先进先出

在使用lrange批量取元素时,首先要明白元素在队列中是如何排列的,这里分为rpush 和 lpush 两种不同的排列方式:

rpush lpush.png

我们可以看到在rpush中,index 与 我们推送元素的顺序是一致,这时候通过lrange取元素的时候,就直接从头取就可以了:

p.lrange(key, 0, n - 1)

取出来的顺序(a,b,c)也是我们推送的顺序,保证了先进先出

但是在lpush中,index 与 我们推送元素是相反的,这时候通过lrange取元素的时候,需要从尾部取:

p.lrange(key, -n, -1)

首先index的写法与rpush不一致,其次拿出来顺序会是(c,b,a),为保证先进先出,我们需要逆序消费

总结

优点

  • 一次网络请求,网络开销小

缺点

  • 需要事务操作,server存在阻塞风险
  • 根据rpush与lpush不同,批量处理方式不同,细节较多,需要考虑清楚

4.pipeline+pop

在了解完 pipeline 后,我们很自然的想到了这种方案:

1
2
3
4
5
css复制代码p:=redis.txpipeline()
for(i:=0;i<n;i++){
p.rpop()
}
values:=p.execute()

该方案本质上就是将一堆pop一口气打包到了server端,让server依次处理,避免了网络开销和事务问题

实质上该方案与方案3,有一个性能的平衡点,即该方案的大量pop带来的性能损耗 == 方案3的事务操作

总结

优点

  • 易实现

缺点

  • 需要考虑pop的数量,无限制的增长会带来大量的性能损耗(相对于方案3,因为每次pop都是查+写)

参考

如何从 Redis 的列表中一次性 pop 多条数据?

Redis系列十:Pipeline详解

Redis中的pipeline和事务的区别是什么?

如何用好redis pipeline

Redis事务与Pipeline功能

本文转载自: 掘金

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

大数据Hadoop-生产调优学习之旅第一篇 一、HDFS 核

发表于 2021-11-18

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

一、HDFS 核心参数

1、NameNode 内存生产配置

  • NameNode内存计算
1
2
3
4
5
6
7
txt复制代码    NameNode内存计算

每个文件块大概占用150byte,一台服务器128G内存为例,能存储多少文件块呢?

128 * 1024 * 1024 * 1024  / 150Byte ≈  9.1亿

G   MB KB   Byte
  • Hadoop2.x系列,配置NameNode内存

NameNode内存默认2000m,如果服务器内存4G,NameNode内存可以配置3g。

在hadoop-env.sh文件中配置如下。

1
sh复制代码HADOOP_NAMENODE_OPTS=-Xmx3072m
  • Hadoop3.x系列,配置NameNode内存

hadoop-env.sh中描述Hadoop的内存是动态分配的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sh复制代码# The maximum amount of heap to use (Java -Xmx).  If no unit
# is provided, it will be converted to MB. Daemons will
# prefer any Xmx setting in their respective _OPT variable.
# There is no default; the JVM will autoscale based upon machine
# memory size.
# export HADOOP_HEAPSIZE_MAX=

# The minimum amount of heap to use (Java -Xms). If no unit
# is provided, it will be converted to MB. Daemons will
# prefer any Xms setting in their respective _OPT variable.
# There is no default; the JVM will autoscale based upon machine
# memory size.
# export HADOOP_HEAPSIZE_MIN=
HADOOP_NAMENODE_OPTS=-Xmx102400m

查看NameNode占用内存

1
2
3
4
5
6
7
8
shell复制代码[moe@hadoop102 bin]$ jps
6816 NameNode
7558 Jps
6951 DataNode
7289 NodeManager
7471 JobHistoryServer
[moe@hadoop102 bin]$ jmap -heap 6816
MaxHeapSize = 1031798784 (984.0MB)

查看DataNode占用内存

1
2
3
4
5
6
7
8
shell复制代码[moe@hadoop102 bin]$  jps
6816 NameNode
7654 Jps
6951 DataNode
7289 NodeManager
7471 JobHistoryServer
[moe@hadoop102 bin]$ jmap -heap 6951
MaxHeapSize = 1031798784 (984.0MB)

查看发现hadoop102上的NameNode和DataNode占用内存都是自动分配的,且相等。不是很合理。

经验参考:docs.cloudera.com/documentati…

image.png

具体修改:hadoop-env.sh

1
2
3
sh复制代码export HDFS_NAMENODE_OPTS="-Dhadoop.security.logger=INFO,RFAS -Xmx1024m"

export HDFS_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS -Xmx1024m"

2、NameNode 心跳并发配置

image.png

hdfs-site.xml

1
2
3
4
5
6
7
xml复制代码The number of Namenode RPC server threads that listen to requests from clients. If dfs.namenode.servicerpc-address is not configured then Namenode RPC server threads listen to requests from all nodes.
NameNode有一个工作线程池,用来处理不同DataNode的并发心跳以及客户端并发的元数据操作。
对于大集群或者有大量客户端的集群来说,通常需要增大该参数。默认值是10。
<property>
<name>dfs.namenode.handler.count</name>
<value>21</value>
</property>

image.png

1
2
3
4
5
6
7
8
9
10
shell复制代码[moe@hadoop102 ~]$ sudo yum install -y python

[moe@hadoop102 ~]$ python
Python 2.7.5 (default, Nov 16 2020, 22:23:17)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import math
>>> print int(20*math.log(3))
21
>>>

3、开启回收站配置

开启回收站功能,可以将删除的文件在不超时的情况下,恢复原数据,起到防止误删除、备份等作用。

  • 回收站工作机制

image.png

  • 开启回收站功能参数说明

(1)默认值fs.trash.interval = 0,0表示禁用回收站;其他值表示设置文件的存活时间。

(2)默认值fs.trash.checkpoint.interval = 0,检查回收站的间隔时间。如果该值为0,则该值设置和fs.trash.interval的参数值相等。

(3)要求fs.trash.checkpoint.interval <= fs.trash.interval。

  • 启用回收站

修改core-site.xml,配置垃圾回收时间为1分钟。

1
2
3
4
xml复制代码<property>
<name>fs.trash.interval</name>
<value>1</value>
</property>
  • 查看回收站

回收站目录在HDFS集群中的路径:/user/moe/.Trash/…

  • 通过网页上直接删除的文件也不会走回收站。
  • 通过程序删除的文件不会经过回收站,需要调用moveToTrash()才进入回收站
1
2
java复制代码Trash trash = New Trash(conf);
trash.moveToTrash(path);
  • 只有在命令行利用hadoop fs -rm命令删除的文件才会走回收站。
1
2
shell复制代码[moe@hadoop102 hadoop]$ hadoop fs -rm -r /input
2021-11-17 23:16:50,135 INFO fs.TrashPolicyDefault: Moved: 'hdfs://hadoop102:8020/input' to trash at: hdfs://hadoop102:8020/user/moe/.Trash/Current/input
  • 恢复回收站数据
1
shell复制代码[moe@hadoop102 hadoop]$ hadoop fs -mv /user/moe/.Trash/Current/input /input

二、HDFS 集群压测

在企业中非常关心每天从Java后台拉取过来的数据,需要多久能上传到集群?消费者关心多久能从HDFS上拉取需要的数据?

为了搞清楚HDFS的读写性能,生产环境上非常需要对集群进行压测。

image.png

HDFS的读写性能主要受网络和磁盘影响比较大。为了方便测试,将hadoop102、hadoop103、hadoop104虚拟机网络都设置为100mbps。

image.png

100Mbps单位是bit;10M/s单位是byte ; 1byte=8bit,100Mbps/8=12.5M/s。

测试网速:来到hadoop102的/opt/module目录,利用python开启一个web服务,方便下边统计测试文件。

1
2
shell复制代码[moe@hadoop102 ~]$ cd /opt/module/
[moe@hadoop102 module]$ python -m SimpleHTTPServer

image.png

1、测试 HDFS 写性能

写测试底层原理

image.png

  • 测试内容:向HDFS集群写10个128M的文件
1
shell复制代码[moe@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -write -nrFiles 10 -fileSize 128MB

Number of files:生成mapTask数量,一般是集群中(CPU核数-1),我们测试虚拟机就按照实际的物理内存-1分配即可

+ Total MBytes processed:单个map处理的文件大小
+ Throughput mb/sec:单个mapTak的吞吐量
+ 计算方式:处理的总文件大小/每一个mapTask写数据的时间累加
+ 集群整体吞吐量:生成mapTask数量\*单个mapTak的吞吐量
+ Average IO rate mb/sec::平均mapTak的吞吐量
+ 计算方式:每个mapTask处理文件大小/每一个mapTask写数据的时间 全部相加除以task数量
+ IO rate std deviation:方差、反映各个mapTask处理的差值,越小越均衡
  • 注意:如果测试过程中,出现异常
+ 可以在yarn-site.xml中设置虚拟内存检测为false
1
2
3
4
5
xml复制代码<!--是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则直接将其杀掉,默认是true -->
<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>
+ 分发配置并重启Yarn集群
  • 测试结果分析

(1)由于副本1就在本地,所以该副本不参与测试

image.png

一共参与测试的文件:10个文件 * 2个副本 = 20个

压测后的速度:1.61

实测速度:1.61M/s * 20个文件 ≈ 32M/s

三台服务器的带宽:12.5 + 12.5 + 12.5 ≈ 30m/s

所有网络资源都已经用满。

如果实测速度远远小于网络,并且实测速度不能满足工作需求,可以考虑采用固态硬盘或者增加磁盘个数。

(2)如果客户端不在集群节点,那就三个副本都参与计算

image.png

2、测试 HDFS 读性能

  • 测试内容

读取HDFS集群10个128M的文件

1
shell复制代码[moe@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -read -nrFiles 10 -fileSize 128MB
  • 删除测试生成数据
1
shell复制代码[moe@hadoop102 mapreduce]$ hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/mapreduce/hadoop-mapreduce-client-jobclient-3.1.3-tests.jar TestDFSIO -clean
  • 测试结果分析

为什么读取文件速度大于网络带宽?由于目前只有三台服务器,且有三个副本,数据读取就近原则,相当于都是读取的本地磁盘数据,没有走网络。

image.png

三、友情链接

大数据Hadoop-Yarn学习之旅第二篇

大数据Hadoop-Yarn学习之旅第一篇

大数据Hadoop-MapReduce学习之旅第六篇

大数据Hadoop-MapReduce学习之旅第五篇

大数据Hadoop-MapReduce学习之旅第四篇

大数据Hadoop-MapReduce学习之旅第三篇

大数据Hadoop-MapReduce学习之旅第二篇

大数据Hadoop-MapReduce学习之旅第一篇

本文转载自: 掘金

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

新来的同事问我 ON DUPLICATE KEY 是什么意思

发表于 2021-11-17

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

写在前面

新来的同事可能经验尚浅,上次问我的where 1=1 是什么意思,这次又来问ON DUPLICATE KEY

上一篇:新来的同事问我where 1=1 是什么意思

首先声明,ON DUPLICATE KEY 是Mysql独有语法。是什么意思?怎么用? 我们往下看

ON DUPLICATE KEY

首先这个的出现一般伴随着UPDATE 语句。我们先来想这样一种场景,我要对数据库中的一条记录,进行新增或者修改,但是呢又不想自己去判断里面是否有,说白了 就是两次查询。通过一行sql搞定。

我们先看下面这种代码,根据用户id新增修改用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//默认不是新增
boolean insert = false;

//先查询用户
User user = UserMapper.selectUserInfoById(id);
if (null == user){
//如果为空 则新建
user = new User();
insert = true;
}
user.setName(name);
user.setAge(age);
if(insert){
return userDao.insert(user);
}
return userDao.update(user)

其实通常的做法有以下几种

1、先SELECT一下,再决定INSERT还是UPDATE;(上面展示的就是这种)

2、直接UPDATE,如果受影响行数是0,再INSERT;

3、直接INSERT,如果发生主键冲突,再UPDATE;

可能我们大多数人都是这样写的,正常逻辑,那么就在想 有没有通过一行sql就可以搞定,不需要通过java代码去查。

那么对MySQL来说其实最好的是直接利用INSERT…ON DUPLICATE KEY UPDATE…语句

1
2
java复制代码INSERT INTO t_user(id,name,age) VALUES (1,'张三',25) 
ON DUPLICATE KEY UPDATE name='张小三',age=30;

到此我们的工作就完成了 ,一条sql搞定新增和修改。当有请求过来的时候。如果id为1 便不会新增,而是会直接修改相应字段

但是这里面还存在一些坑

注意点

看下面这段代码会如上所述吗

1
2
java复制代码INSERT INTO t_user(name,age) VALUES ('张三',25) 
ON DUPLICATE KEY UPDATE name='张小三',age=30;

id是自增,这里我们省略赋值,那么结果会失效的,只会新增,update会失效,因为没有体现出key

原因(总结)

MySQL文档里说明:ON DUPLICATE KEY UPDATE语句判断是否冲突是依靠主键或唯一索引,因此sql当中必须体现出键,其实原理很简单嘛,mysql 不能出现相同key DUPLICATE是重复的意思,所以最终就可以理解成 当出现重复key时便修改

那么同事问了,那我不想赋值id呢,mysql文档也说了,判断是否冲突是依靠主键或唯一索引,那么我们就加唯一索引就好了,根据自己的业务情况而定,找个字段弄个唯一索引,那么就可以实现,比如我们给我们表创建唯一索引

1
java复制代码CREATE UNIQUE INDEX IDX_NAME ON t_user(name);

顺利实现

1
2
java复制代码INSERT INTO t_user(name,age) VALUES ('张三',25) 
ON DUPLICATE KEY UPDATE name='张小三',age=30;

好,今天就到这里,我们下期再见

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

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

Java数据结构入门以及数据结构和算法的关系 1 基本概念和

发表于 2021-11-17

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

简单介绍了数据结构的相关入门概念,以及数据结构的总体的分类,最后介绍了数据结构和算法的关系。

广义的数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。

狭义的数据结构是相互之间存在一种或多种特定关系的数据元素的集合。

1 基本概念和术语

1.1 数据

数据是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。

数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型,对于数值类型,可以进行数值计算;对于非数值类型是通过编码的手段变成数值数据来处理的。

1.2 数据元素

数据元素是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。比如人、牛、狗、猪。

1.3 数据项

数据项是组成数据元素的独立的不可分割的最小基本单位。一个数据元素可以由若干个数据项组成。

比如人这样的数据元素,可以有眼、耳、鼻、嘴、手、脚这些数据项,也可以有姓名、年龄、性别、出生地址、联系电话等数据项,具体有哪些数据项,要视你做的系统来决定。

1.4 数据对象

数据对象是性质相同的数据元素的集合,是数据的子集。比如,人都有姓名、生日、性别等相同的数据项,人属于数据。

在实际应用中,处理的数据元素通常具有相同性质,在不产生混淆的情况下,我们都将数据对象简称为数据。

以上概念的关系图如下:

在这里插入图片描述

2 数据结构

数据结构是计算机存储、组织数据的方式,是指相互之间存在一种或多种特定关系的数据元素的集合。包括按照视点的不同,可以分为逻辑结构和物理结构。

2.1 逻辑结构

逻辑结构表示数据元素之间的抽象关联关系。逻辑结构是针对具体问题的,是为了解决某个问题,在对问题理解的基础上,选择一个合适的数据结构表示数据元素之间的逻辑关系。

常见逻辑结构有三种基本类型:线性结构、树形结构和图形结构,也可以统一的分为线性结构和非线性结构。

线性结构:

线性结构中的数据元素之间是一对一的关系,如下图:

在这里插入图片描述

树形结构:

树形结构中的数据元素之间存在一对多的关系,如下图:

在这里插入图片描述

图形结构:

图形结构中的数据元素之间存在多对多的关系,如下图:

在这里插入图片描述

2.2 物理结构

物理结构是指数据的逻辑结构在计算机中的具体的存储形式,也称作存储结构。大概分为两种:顺序存储结构和链式存储结构。

顺序存储结构:

是指数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。比如数组的存储,就是采用的顺序存储结构,数组的存储单元是连续的。

在这里插入图片描述

链式存储结构

是指数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素通过指针保持逻辑上的联系。比如链表的常见实现就是采用的链式存储结构。

逻辑结构是面向问题的,而物理结构就是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。

在这里插入图片描述

3 抽象数据类型(ADT)

3.1 数据类型

是一个值的集合和定义在这个值集上的一组操作的总称。

3.2 抽象

抽取出事物普遍的性质、特征,忽略了具体的性质、特征。

3.3 抽象数据类型(Abstract Data Type,ADT)

是指一个有类似行为的特定类别的数据结构的数学模型数学模型以及定义在该模型上的一组操作。是理论的工具。

抽象数据类型目的是使人们能够独立于程序的实现细节来理解数据结构的特性。抽象数据类型的定义取决于它的逻辑特性,而与计算机内部如何表示无关。

例如:抽象的栈(stack)是一个先进后出的队列,以及由3个操作定义:推入push,弹出pop,查看堆栈顶端数据peek。不同的语言的实现栈的方式不同,但是他们的抽象数据类型是一样的。

每种数据结构都有它的抽象数据类型,要学习数据结构就必须了解它的抽象数据类型。例如现在很多的数据结构和算法书籍,它们使用的是C语言来实现的,但是如果了解了数据结构的抽象数据类型,我们就可以使用Java语言来实现了。

4 数据结构与算法关系

数据结构操作的对象是数据元素,即他们有相同的属性,它们之间的存在的关系会产生不同的结构,数据元素之间的关系+操作构成了数据类型,对已有的数据类型进行抽象就构成了抽象数据类型(ADT),就是封装了值和操作的模型。

操作数据的一组代码就叫算法,我们可以使用相应的算法编写一段代码,形成相应的数据结构。简单地说,一种数据结构具体的实现,是通过相应的的代码(算法)实现的,而后对数据结构中的数据进行某些操作的代码,比如查找,也是用到了算法。

数据结构是为算法服务的,算法也要作用在特定的数据结构之上才有意义。

对于一组数据,我们想到实现某个目的,先要考虑采用什么样的数据结构取存储这些数据,然后使用什么样的算法来操作这些数据,而在数据结构的构建过程中也是使用了相应的算法。比如我们需要对数据进行排序,那么我们可以采用红黑树或者堆这样的数据结构来存储,然后就看我们怎样的算法实现了。

线性表,堆栈,串,树,图是常见的用抽象数据类型定义的数据结构,查找和排序是常见的算法。

关于算法的入门,可以看看这篇文章:算法入门以及时间复杂度推导。

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

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

用Redis实现发布订阅

发表于 2021-11-17

核心代码:

主要有三步:

  1. 获取主题
  2. 主题添加订阅者
  3. 通过主题发布事件
1
2
3
4
5
6
7
8
9
10
java复制代码RTopic<SomeObject> topic = redisson.getTopic("anyTopic");
topic.addListener(new MessageListener<SomeObject>() {
@Override
public void onMessage(String channel, SomeObject message) {
//..
.}
});
// in other thread or JVM
RTopic<SomeObject> topic = redisson.getTopic("anyTopic");
long clientsReceivedMessage = topic.publish(new SomeObject());

具体过程

订阅者实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Component
public class ProjectMessageRedisListener implements MessageListener<ContractPassedEvent> {
public static final String TOPIC = "ContractPassedEvent";
@Autowired
RedissonClient redissonClient;

@Override
public void onMessage(CharSequence channel, ContractPassedEvent msg) {
System.out.println("收到事件:" + msg);
}
}


//必须实现序列化接口
@Data
public class ContractPassedEvent implements Serializable {
private static final long serialVersionUID = -1L;
private String name;
}

注册订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Configuration
@EnableAsyncpublic
class WebMvcConfigure implements WebMvcConfigurer {
@Autowired
RedissonClient redissonClient;

@Bean("contractPassedTopic")
public RTopic contractPassedTopic(){
//注册监听
RTopic topic = redissonClient.getTopic(ProjectMessageRedisListener.TOPIC);
topic.addListener(ContractPassedEvent.class, projectMessageRedisListener);
return topic;
}

发布事件

1
2
3
4
5
6
7
8
9
10
11
less复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = WebSpringBootTestApplication.class)
public class ProjectMessageRedisListenerTest{
@Autowired
RTopic topic;

@Test
public void testRedisTopic(){
topic.publish(new ContractPassedEvent("A00-Nj-2021-01-289-002932"));
}
}

订阅到多个主题

1
2
3
4
5
6
7
8
typescript复制代码// subscribe to all topics by `topic1.*` pattern
RPatternTopic<Message> topic1 = redisson.getPatternTopic("topic1.*");
int listenerId = topic1.addListener(new PatternMessageListener<Message>() {
@Override
public void onMessage(String pattern, String channel, Message msg) {
Assert.fail();
}
});

原理

订阅:

Image [2].png

发布:

Image.png

Redis 发布订阅功能的特性

Topic 模式监听器在重连到 Redis 服务器或者 Redis 服务器故障恢复时自动重新订阅。

  1. 消息的发送者与接收者之间通过 channel 绑定:channel 可以是确定的字符串,也可以基于模式匹配
  2. 客户端可以订阅任意多个channel
  3. 发送者发送的消息无法持久化,所以可能会造成消息丢失
  4. 由于消息无法持久化,所以,消费者无法收到在订阅channel之前发送的消息
  5. 发送者与客户端之间的消息发送与接收不存在 ACK 机制

Redis 发布订阅功能的适用场景

由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。例如:消息推送,内网环境的消息通知等

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

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

推荐几款珍藏的谷歌插件,好用到爆

发表于 2021-11-17

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

谷歌浏览器(Google Chrome)不仅拥有极简界面、运行速度快、稳定性高等优点,还有一个优势就是其拥有强大的扩展能力,合理使用插件便可以实现多种功能。

下面我就推荐几款珍藏多年的插件

Google翻译

其实谷歌浏览器就内置了这款插件,当你浏览英文网站时会自动检测,出现按钮时你一点击,就会翻译为中文,对于英语渣的朋友来说简直是福音。

同时可以鼠标划词翻译,并且有多重读音可以选择

Adblock Plus

这是一款屏蔽广告的插件,可以屏蔽大部分网站的烦人广告,还你一个整洁的页面。

会屏蔽掉:

1
2
3
4
5
bash复制代码· 横幅广告
· 视频广告
· Facebook 广告
· 弹出窗口广告
· 所有其他显眼的广告

随便选一个在线电影的界面对比看下效果:

没使用前:

使用后,屏蔽了所有能看到的广告:

OneTab

OneTab,顾名思义,就是把你的多个标签页合并为一个,有时候搜索资料打开太多标签时,使用这个就比较方便了。号称可以解决95%的内存,对于像我这种一查资料就打开几十个窗口的人还是很有用的

CSDN·浏览器助手

虽然CSDN网站有很多质疑,但是这款插件还是蛮好用的,首页界面清爽美观,把常用的网站和工具集成到一起,页面背景类似bing可以每天更新

Print Friendly & PDF

这个插件也很好用,可以页面完全导出为PDF格式,导出时还可以自定义节选内容,对买了类似极客时间的限时课程,规定时间无法学完的就可以先打印成PDF到本地

Tampermonkey

这款油猴插件是最受欢迎的浏览器扩展之一,它是一个脚本管理器,你可以通过网站(greasyfork.org/zh-CN/scrip…)下载各种脚本安装实现多种功能,比如网页端网盘直接下载、去视频广告等都可以通过这个实现。

GitHub加速

对于下载github代码很慢的推荐安装GitHub加速,可以大幅提高下载速度,不过对于浏览网页时,图片无法显示的问题这个插件并不能解决

JSON-handle

对JSON格式的内容进行浏览和编辑,以树形图样式展现JSON文档,并可实时编辑。我认为是最好的Json插件了,可以直接格式化页面返回, 对于经常调试接口来说还是很美观的,效果如图

Markdown Nice

这款插件包含多种主题格式,对Markdown文档有很好的的美化效果,特别适合在公众号发文时使用

Chrome版Todoist

使用Chrome版Todoist可以添加网页为任务,添加一篇博文到阅读清单,计划一天,直接在插件中便捷地管理任务。非常清晰地任务列表,便于任务和时间管理,有计划的进行学习和工作。

谷歌学术

对于访问不了谷歌的可以安装此插件,用于学术方面的查询,提供谷歌搜索,gmail,Chrome Store,Android/Golang等谷歌产品访问, 而且不用修改浏览器主页,日常学术已经够用了

小结

上面提到的插件都可以直接在谷歌应用商店中下载,如果嫌麻烦也可以在公众号后台回复暗号9527就可以打包获取到,当然还有很多实用插件可能我没提到,下次有机会再补充,也欢迎大家下方留言~

本文转载自: 掘金

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

OAuth 20(三):令牌机制 一、OAuth 20

发表于 2021-11-17

大家好,我是橘长,昨天分享了 「OAuth 2.0 中的授权许可协议 」,大家可以自行回顾一下具体是哪四种,每一种的特点、适用的业务场景,最完备的授权码许可机制的流程是什么。

今天橘长给大家分享「 令牌机制 」。

一、OAuth 2.0 的核心是什么?

回顾之前 OAuth 第一篇文章中用户访问客户的故事,有一个关键名词临时访问二维码

当用户出示一系列信息和操作之后拿到这个二维码,它就可以去见到客户了。

这个二维码回到 OAuth 中,其实就是令牌,那么具体来看令牌到底是什么呢?

其实就是授权之后的结果,前面做的一系列操作都是为了获取最终这个令牌,有了令牌凭据,才能去访问这个令牌能请求的路由、服务、资源等。

可见 OAuth 2.0 的核心就是颁发访问令牌,使用访问令牌。

二、OAuth 2.0 中的令牌

OAuth 官网没有限定令牌的格式,但目前只支持一种,就是 bearer 令牌。

可以是随机字符串,也可以是自定义的数据结构,但都要符合三个条件。

唯一性、不连续、不可猜。

三、JWT

1、是什么

JSON Web Token 是一种基于 RFC7519 协议的开放标准,定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全的传输数据。

简单理解:其实是一组自定义的结构化数据,通过结构化的方式去生成 token

一方面是使得返回给客户端的数据有意义

另外一方面是自编码能力,扩展性强

此外还可以通过签名来保护数据安全,JWT 是需要在 HTML 和 HTTP 等环境下传输的

一方面是需要做 URLEncode(编码)保障不乱码且不丢失数据

另外一方面是做加密,避免失窃

官方文档:jwt.io/introductio…

橘长对其做了翻译:github.com/AFlymamba/t…

2、组成

内部包含了三部分,头部、有效负荷、签名,三部分之间通过 .(点) 连接

如下是一个简单的 jwt 示例:

1
css复制代码header.payload.signature

1)header(头部)

JWT 的第一部分是 Header(头部),通常由两部分组成,type 和 sign

type 表示令牌类型,在此自然是”JWT“

此外 sign 表示采用的签名算法,可以是 HMAC SHA256 或 RSA 等

如下是一个简单示例:

1
2
3
4
json复制代码{
"type": "JWT",
"sign": "HS256"
}

最后生成的头部是需要把这一串 JSON 串做 URLEncode 操作,得到一个字符串。

1
2
ini复制代码JSONObject headerJson = JSONUtil.createObj();
String headerStr = base64UrlEncode(headerJson);
  • payload(有效负荷)

JWT 的第二部分是 Payload(有效负荷),这是 JWT 最有价值的一部分

其中包含声明(Claims),声明是关于实体(通常是用户)和其他数据的声明

简单理解就是自定义的那部分数据。

JWT 官方有三种类型的声明,分别是 注册声明、公开声明、私有声明。

注册声明:一组框架预定义的声明,非强制性要求每个都实现,但建议使用,通过这个可以提供一组有用的、可操作的数据。

公开声明:JWT 使用者自定义的,但是需要注意不要和官方提供的数据相冲突。

私有声明:适用场景是数据传输,当通信双方约定好数据结构之后,做共享数据用。

【注意】Payload 的声明只有三个字符长度,符合 紧凑型 特点。

特别要来学习官方推荐的:注册声明

挑选了场景的一些数据定义做示例

iss:issuer(JWT 的发行者)

exp:expression time(JWT 过期时间)

sub:subject(主体)

aud:audience(受众)

如下给出一个 payload 的 json 结构:

1
2
3
4
5
json复制代码{
"sub": 1,
"name": "John",
"admin": true
}

最后生成的 payload 是需要把这一串 JSON 串做 URLEncode 操作,得到一个字符串。

1
2
ini复制代码 JSONObject payloadJson = JSONUtil.createObj();
String payloadStr = base64UrlEncode(payloadJson);
  • signature(签名)

JWT 的第二部分是 Signature(签名),JWT 需要在网络上传输

有了头部和有效负荷携带数据,为了保证安全性,需要做相关加密算法

签名其实就是对 header.payload 数据做加密后的结果。

创建签名前,后续获取到 header、payload、做签名用到的 secret、签名算法,如下是一个生成签名的示例:

1
2
3
4
5
6
7
8
ini复制代码String headerStr = base64UrlEncode(header);
String payloadStr = base64UrlEncode(payload);
String secret = "xxxxx";
String headerPayload = headerStr + "." + payloadStr;


// 生成签名
String signature = HMACSHA256(headerPayload, secret);

签名作用有哪些?

①保证 header 和 payload 在传输过程中没有被纂改。

②如果签名算法用的是非对称加密(比方 RSA),可以通过验证签名,来确定谁是持有私钥的那方。

整合三部分信息,其实就回到了什么是 JWT,其实就是一个由 header.payload.signature 组成的字符串。

3、如何使用 JWT

假设是用户授权登录场景,当用户登录成功之后服务端会颁发 token 给到客户端

之后用户的每个请求都需要携带 token,如果没携带那么就认为没有凭据,无法访问,如果携带合法合理的,放行。

思考:token 的提交方式?

OAuth 官方有三种方式,如下

方式一:Form-Encoded Body Parameter(表单参数)

form.png

方式二:URI Query Parameter(URI查询参数)

uri.png

方式三:Authorization Request Header(授权请求头部字段)

header.png

4、JWT 的优缺点

万物都有两面性。

优点:

1)相比随机字符串,具有含义(自编码能力)

2)加密,保护了 header、payload 传输过程被盗窃

缺点:

最大缺点:覆水难收

JWT 从服务端颁发之后,在有效期内其实是横冲直撞的,它不存储在服务端,很难改变 JWT 的状态。

切忌在代码中不要开 api 提供给测试同事获取token,如果要获取,开发手动生成。

四、开源组件

1、为什么选用开源

JWT 中的 header、payload 都需要做 base64URLEncode 操作,为了保证传输过程的安全,还需要引入加密算法做签名,如果自行手写极容易出错,同时安全风险高。

开源组件有一个特点那就是开箱即用,它屏蔽了底层复杂的具体实现(比方说封装了 Base64 操作、对称/非对称一系列算法实现等),让开发者更专注于上层 api 调用和业务实现。

在 JWT 这块选用开源组件,我们会比较关心两块,一块是 api 层面,另外一块自然就是开源活跃度。

api 层面:是否提供了 生成 JWT 的接口、校验 JWT 的接口等。

开源活跃度:说明出了 bug 有处可寻。

2、JJWT

官网:github.com/jwtk/jjwt

JJWT.png

以下示例说明在 SpringBoot 中如何简单使用 JJWT:

1)引入依赖

1
2
3
4
5
6
xml复制代码<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>

2)生成 JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码# 声明
Map<String, Object> claims = new HashMap<>(2);
claims.put("authId", activityUser.getId());
claims.put("authRole", "user");


String jwt = Jwts.builder()
.setClaims(claims)
.setSubject("api")
.setIssuedAt(new Date())
.setExpiration(expirationAt)
.signWith(SignatureAlgorithm.HS512, signingKey)
.compact();

3)使用 JWT

这里用 Authorization Header 头部的方式提交 jwt

postman.png

4)校验 JWT

①自定义 SpringMVC 拦截器,在 preHandler 方法中做拦截

  • 自定义拦截器
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
java复制代码public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandler(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 方便演示,硬编码
String tokenKey = "Bearer";
String tokenSecret = "xxx";
// 1、从 request 头部获取 token
String token = request.getHeader(tokenKey);
// 方便演示,去掉了 null 等判断
// 2、调用 Jwts 开源工具提供的方法,获取声明
Claims claims = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(tokenKey).getBody();
// 判断是否过期等,硬编码
boolean judgeResult = this.isNullOrExpired(claims);

// 后续业务逻辑
}
}


/**
* 判断令牌是否过期
*
* @param claims 令牌解密后的Map对象。
* @return true 过期,否则false。
*/
public boolean isNullOrExpired(Claims claims) {
return ObjectUtil.isNull(claims) || DateUtil.date().after(claims.getExpiration());
}
  • 注入 Spring 容器
1
2
3
4
5
6
7
8
9
10
11
typescript复制代码@Component
public class InterceptorConfig implements WebMvcConfigurer {


@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor ())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/oauth/login");
}
}

②校验不通过的时候,后端响应 401

1
2
3
4
json复制代码{
"message": "用户会话已过期,请重新登录!",
"statusCode": 401
}

③校验通过,进入业务接口

五、总结

今天橘长带大家分析了 OAuth 2.0 的核心 以及 令牌机制,大家需要记住几点:

1、OAuth 2.0 的核心是颁发令牌,使用访问令牌。

2、令牌组成、payload 中的三种声明,JWT 的优缺点。

3、开源 JWT 组件 JJWT 的使用。

下一篇橘长将给大家带来「 手把手带你落地实现 OAuth 2.0 中的四大角色」的解读,感谢你的关注,如果你觉得有所收益,欢迎点赞、转发、评论,感谢认可!

本文转载自: 掘金

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

ThreadLocal使用及简单原理分析

发表于 2021-11-17

在阅读Spring源码时, 有注意到一个类ThreadLocal出现的次数很多, 其实ThreadLocal的应用是很广泛的, 不仅仅在Spring里, 在Mybatis中也很普遍, 在一些项目的业务代码也可能会看到他的身影.

其实他的作用, 就是一个线程局部变量, 但是因为大多数的业务编程情况不常用到, 所以可能我们比较陌生, 现在分析一下, 以便在代码里看到时, 不会阻碍我们的阅读.

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码//示例一
//创建, 并赋初始值
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr");

//线程1
new Thread(() -> {
//打印默认值
System.out.println(threadLocal.get());
//赋值
threadLocal.set(Thread.currentThread().getName());
//打印值
System.out.println(threadLocal.get());
}).start();

//线程2
new Thread(() -> {
//打印默认值
System.out.println(threadLocal.get());
//赋值
threadLocal.set(Thread.currentThread().getName());
//打印值
System.out.println(threadLocal.get());
}).start();

输出值:

image.png

上述代码主要展示了创建、初始化、获取值、赋值的几个操作, 使用是很简单的.

下面主要分析比较复杂的能做什么, 怎么实现的问题.

分析特性

ThreadLocal其实比较简单, 方法也不多, 比较常用的也就是get set remove initialValue, 在分析这些方法时, 我们先要理解, ThreadLocal是用来做什么的.

源码对其的定义如下:

1
2
3
4
arduino复制代码此类提供线程局部变量。 
这些变量不同于它们的普通对应物,
因为每个访问一个(通过其get或set方法)的线程都有自己的、独立初始化的变量副本。
ThreadLocal实例通常是希望绑定与线程相关的类中的私有静态字段(例如,用户 ID 或事务 ID)。

也就是说, 其主要是为了在一个线程中共享属性, 而具体是怎么实现的呢? 看下面的示例代码, 慢慢分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码//示例二
ThreadLocal<HashMap<String, Object>> threadLocal = ThreadLocal.withInitial(() -> new HashMap<>());

new Thread(() -> {
HashMap<String, Object> map = threadLocal.get();
map = map == null ? new HashMap<>() : map;
map.put("current1", Thread.currentThread().getName());
threadLocal.set(map);
System.out.println("t1:" + JSONObject.toJSONString(threadLocal.get()));
}).start();

new Thread(() -> {
HashMap<String, Object> map = threadLocal.get();
map = map == null ? new HashMap<>() : map;
map.put("current2", Thread.currentThread().getName());
threadLocal.set(map);
System.out.println("t2:" + JSONObject.toJSONString(threadLocal.get()));
}).start();

System.out.println("main:" + JSONObject.toJSONString(threadLocal.get()));

输出值:

image.png

可以看出, 两个线程对同一个threadLocal对象进行操作, 线程1和线程2同样的代码, 打印出的却是各自的值, 没有相互影响, 尤其是示例一线程2的第一句(打印默认值), 在线程1已经对threadLocal做过操作以后, 打印的还是初始化的默认值, 可见在对个线程中是不会相互污染的.

这也可以体现出其特性:线程内变量共享

实现的主要思想是, 我们定义的一个ThreadLocal在多个线程间, 会在每个线程里都创建一个副本, 每个副本是归属于线程的, 这样就做到了多个线程之间的互不干涉, 具体怎么实现一个对象在多个线程里创建副本的呢?

image.png
看上图, 由ThreaLocal的结构可以看出, ThreadLocal本身没有定义变量存储值, 那我们set的值在哪里存放呢?
先抛出概念, 后续我们看具体代码:

ThreadLocal只是做了值的映射维护, 真正的值是存储在Thread类的threadLocals字段里的

基于上面的概念, 我们不难理解, 既然值都存在每个线程的Thread类里, 那做到线程间的隔离就很正常了.

下面通过具体的方法来印证这个概念.

set方法

先看源码:

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取存储值的对象
ThreadLocalMap map = getMap(t);
if (map != null)
//不为空则set值
map.set(this, value);
else
//未初始化则初始化
createMap(t, value);
}

这里涉及以下几个点:

  • getMap 获取值, 是从哪里获取的
1
2
3
java复制代码ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

可以直观的看出, 我们获取的ThreadLocalMap是从Thread中取值的.

  • createMap 为什么在获取值的时候才初始化? 初始化做了什么
1
2
3
java复制代码void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

很简单, 创建了一个ThreadLocalMap对象并赋值给threadLocals, 这里要主要入参: ThreadLocalMap的构造函数的第一个入参是ThreadLocal的当前对象引用

  • ThreadLocalMap 存储值的结构

ThreadLocalMap是一种定制的哈希映射,使用 WeakReferences(Java弱引用) 作为键(Entity扩展了WeakReference)

1
2
3
4
5
6
7
8
9
10
11
java复制代码ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建条目* table是一个数组
table = new Entry[INITIAL_CAPACITY];
//计算下标, 并赋值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
//记录条数
size = 1;
//调整容量阀值
setThreshold(INITIAL_CAPACITY);
}

可以看出, table是在ThreadLocalMap有第一个值要存放时, 才会被创建, 这说明其是惰性创建的.

而说ThreadLocalMap使用 WeakReferences 作为键, 是因为Entry的特性决定

1
2
3
4
5
6
7
8
9
scala复制代码    static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

可看出, Entry扩展了WeakReference(Java弱引用), 而构造Entry的构造函数, key是ThreadLocal对象

这里涉及到了WeakReference相关的知识, 在这里不做过多说明

get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//不为空, 则获取值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//值不为空, 则返回关联的值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap为空, 或对应Entity为空, 则进行初始化
return setInitialValue();
}

有get方法的研究可知获取线程和getMap(t)所做的工作, 这里不再重复, 下面几个地方我们要分析一下:

  • 获取Entry及获取Entry关联的值

我们通过上面的分析, 可以知道:ThreadLocalMap的存储, 和Entry的存储, 获取就是通过存储的相同规则去反向获取值, 例如map.getEntry(this)使用当前ThreadLocal自身的引用.

  • setInitialValue方法做了什么
1
2
3
4
5
6
7
8
9
10
Java复制代码private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

setInitialValue方法用做获取值时仍旧未初始化的情况下初始化, 这个方法是set方法的变体, 所以基本和set方法一致, 但仍有不同, 如下:

    • value的值由指定
这里的initialValue是可以自己覆盖的, 当我们手动指定了初始化的值时(我们只需要覆盖initialValue方法), 在第一次获取时, 如果未设置值, 就会使用我们覆盖的initialValue方法的返回值来初始化.
initialValue方法默认是返回null的.



1
2
3
Java复制代码   protected T initialValue() {
return null;
}
    • 有返回值, 返回的是initialValue返回的值

由此, ThreadLocal的主要特性已经展现了, 我们再分析几个常用的方法, 更全面的认识ThreadLocal类.

remove方法

1
2
3
4
5
Java复制代码public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

remove方法用来移除当前set的值, 在移除后再次通过get获取, 会重新初始化initialValue方法

initialValue方法

在get()方法中已经介绍, 这里展示两种覆盖的常用方法

  • 匿名内部类
1
2
3
4
5
6
Java复制代码ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "base";
}
};
  • withInitial
1
Java复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr");

withInitial方法

上述示例里用到了ThreadLocal.withInitial覆盖initialValue, 怎么实现的呢? 分析一下:

1
2
3
Java复制代码public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}

我们可以看到通过Supplier, 返回了一个SuppliedThreadLocal, 分析下SuppliedThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码//扩展了ThreadLocal
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

private final Supplier<? extends T> supplier;

SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}

//覆盖了ThreadLocal的initialValue方法
@Override
protected T initialValue() {
return supplier.get();
}
}

可见SuppliedThreadLocal其实也是扩展了ThreadLocal且覆盖了initialValue方法, 这些都可以和我们上述的分析对应上, 那么supplier.get()是怎么返回ThreadLocal.withInitial(() -> “baseStr”)中的baseStr的呢?

1
2
3
4
csharp复制代码@FunctionalInterface
public interface Supplier<T> {
T get();
}

可见Supplier是个函数式接口, 那么

1
ini复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "baseStr");

等价于

1
2
3
4
5
6
typescript复制代码ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
return "baseStr";
}
});

现在看起来, 就顺理成章多了

这里提出这个, 和ThreadLocal无关, 只是避免lambda表达式对阅读分析起到干扰, 故而提及


后续, 将会列举一些经典的使用案例, 对于原理分析就记录到这.

本文转载自: 掘金

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

MIT 6824 Lab4A The Shard Cont

发表于 2021-11-17

Lab3实现的KV服务,只有一个Leader接收所有的请求,在Lab4中,将所有请求分配到多个Leader-Followers集群中,每个Leader处理属于它这一集群管理的请求。

请求的划分方式有很多种,例如Key以’A’开头的请求都交给某一Leader-Followers集群处理,实验中,测试程序负责提供划分方案。

本文中,同样也是在代码中,Group表示一个Leader-Followers集群,Gid为它的标识,Shard表示所有请求的一个子集,Config表示一个划分方案。本实验中,所有请求分为NShards = 10份,Server给测试程序提供四个接口。

  1. Join:为某个Group添加节点,或添加Group。
  2. Leave:移除某个Group。
  3. Move:某个Shard分配给某个Group处理。
  4. Query:上面三个操作都会改变当前Config,Query返回某个历史版本的Config。

Controller Server也会组成一个集群,使用Raft做容灾处理,因此,Controller Server和KVServer是一样的结构。只不过是从Get、Put、Append变成了Join、Leave、Move、Query,因此直接把Lab3A的代码复制过来,再稍加改动就OK了。

Server

configs存储了所有的config版本,每一次进行Join或Leave或Move都会基于当前config追加一个新的config到configs中,之所以要保存所有的config,主要是为了测试程序检查。

Num就是版本编号,Shards记录当前方案,每个shard由哪个group负责处理,这个分配方案应该是均衡的,所以每一次config的变化都要重新分配,使Shards平均。

Groups记录了每个group中都有哪些节点。

1
2
3
4
5
6
7
8
9
go复制代码type ShardCtrler struct {
configs []Config
}

type Config struct {
Num int // config number
Shards [NShards]int // shard -> gid
Groups map[int][]string // gid -> servers[]
}

Join

先获取当前config,即configs中最后一个config,然后基于这个config做修改,把新Join的server加入到config.Groups中,然后把config追加到configs后。

1
2
3
4
5
6
7
go复制代码func (sc *ShardCtrler) Join(servers map[int][]string) {
config := sc.GetConfig()
for gid, group := range servers {
config.Groups[gid] = append([]string(nil), group...)
}
sc.configs = append(sc.configs, config)
}

Leave

和Join是类似的,删除Groups中的某个group。

1
2
3
4
5
6
7
go复制代码func (sc *ShardCtrler) Leave(gids []int) {
config := sc.GetConfig()
for _, gid := range gids {
delete(config.Groups, gid)
}
sc.configs = append(sc.configs, config)
}

Move

Move只需要修改Shards结构即可。和Lab3一样,因为Query是读请求,所以在RPC中直接返回configs[num]即可。

1
2
3
4
5
go复制代码func (sc *ShardCtrler) Move(shard int, gid int) {
config := sc.GetConfig()
config.Shards[shard] = gid
sc.configs = append(sc.configs, config)
}

Balance

每一次写请求后都需要修改Shards保证所有group负责的shard数量最大和最小之差不超过1,这里先收集所有group负责的shard数量,然后进行排序。

先算出平均每个group应该负责多少个shard,多的拿出来,少的加进去,为了保证每个Controller Server动作一致,所以shard数量相等的,按照gid排序。

这里给出Balance的代码,go不太熟悉,写的比较乱,但能过测试就行了。

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
go复制代码func (sc *ShardCtrler) Balance() {
config := sc.configs[len(sc.configs)-1]
if len(config.Groups) == 0 {
return
}
m_gid_shardnum := make(map[int]int)
unassigned := make([]int, 0)
avgShardNum := NShards / len(config.Groups)

for gid := range config.Groups {
m_gid_shardnum[gid] = 0
}
for shard, gid := range config.Shards {
if _, ok := config.Groups[gid]; !ok {
unassigned = append(unassigned, shard)
continue
}
if m_gid_shardnum[gid] < avgShardNum {
m_gid_shardnum[gid]++
} else {
unassigned = append(unassigned, shard)
}
}

s_gid_shardnum := make([][2]int, 0)
for gid, shardnum := range m_gid_shardnum {
s_gid_shardnum = append(s_gid_shardnum, [2]int{gid, shardnum})
}

sort.Slice(s_gid_shardnum, func(i, j int) bool {
if s_gid_shardnum[i][1] == s_gid_shardnum[j][1] {
return s_gid_shardnum[i][0] < s_gid_shardnum[j][0]
}
return s_gid_shardnum[i][1] < s_gid_shardnum[j][1]
})

base := 0
for _, v := range s_gid_shardnum {
gid := v[0]
shardnum := v[1]
for i := 0; i < avgShardNum-shardnum; i++ {
config.Shards[unassigned[base+i]] = gid
}
if avgShardNum-shardnum > 0 {
base += avgShardNum - shardnum
}
}

for _, v := range s_gid_shardnum {
if base >= len(unassigned) {
break
}
gid := v[0]
config.Shards[unassigned[base]] = gid
base++
}
sc.configs[len(sc.configs)-1].Shards = config.Shards
}

实验总结

Lab4A和Lab3A差不多,所以也比较简单。唯一要注意的点是,如果你不熟悉go语言,需要了解一下go的传值和传引用相关的坑,go中,切片和map都是传引用的,所以要注意深拷贝的问题。

image.png

最后,为了证明我不是在乱写,附上我的测试结果。

本文转载自: 掘金

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

Python面向对象编程03:类继承和其衍生术语 Pytho

发表于 2021-11-17

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

正式的Python专栏第38篇,同学站住,别错过这个从0开始的文章!

前篇学委展示分享了面向对象编程和深度认识类的结构,最后稍微提到了继承。

这次我们把继承和继承衍生的术语一并解释

Python支持单继承,多继承

趁着印象比较深,学委拿了前篇文章的代码稍作修改:

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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello

"""
下面是一个程序员类定义
"""


class Programmer():
def code(self):
print("life is short, why not python?")

"""
下面是一个学生类定义
"""


class Student(object):
"""这里是一个学生类定义"""

def __init__(self, name):
self.name = name

def get_name(self):
return self.name

def set_name(self, name):
self.name = name

def study(self):
print(f"{self.name} : 好好学习,天天向上!")


# 多继承(父类1,父类2)可以更多父类
class PrimarySchoolStudent(Student, Programmer):
pass


print("*" * 16)
xiaopengyou = PrimarySchoolStudent("一个小学生(学委的一个粉丝)")
xiaopengyou.study()
xiaopengyou.code()

print("类的基类:", PrimarySchoolStudent.__bases__)

通过多继承,我们轻松创建了一个类PrimarySchoolStudent,既是学生又是程序员!

1
kotlin复制代码class PrimarySchoolStudent(Student, Programmer)

这是运行结果:

屏幕快照 2021-11-17 下午11.34.37.png

可以看到,非常轻松就创建了一个具备两个类行为和属性的新类PrimarySchoolStudent。

下面这段补充一下,Python提供了工具函数‘issubclass’方便我们识别手中的类 跟 某个类的关系。

‘isinstance’ 也帮助我们识别某个对象跟某个类的关系。

1
2
3
4
5
6
7
8
9
10
python复制代码# 判断对象是由哪个类衍的子类吗
print("PrimarySchoolStudent 是 Programmer 的子类吗 ?", issubclass(PrimarySchoolStudent, Programmer))
print("PrimarySchoolStudent 是 Student 的子类吗 ?", issubclass(PrimarySchoolStudent, Student))
print("Student 是 Programmer 的子类吗 ?", issubclass(Student, Programmer))

# 判断对象是由哪个类创建出来的实例
print("xiaopengyou 是 PrimarySchoolStudent 的实例 ?", isinstance(xiaopengyou, PrimarySchoolStudent))
print("xiaopengyou 是 Programmer 的实例吗 ?", isinstance(xiaopengyou, Programmer))
print("xiaopengyou 是 Student 的实例吗 ?", isinstance(xiaopengyou, Student))
print("xiaopengyou 是 dict 的实例吗 ?", isinstance(xiaopengyou, dict))

读者尽量复制运行这段代码加深一下印象。

有时候子不愿承父志,子类可以选择覆盖 - Overriding

比如这个小学生这个类,想要吧study函数重写为玩游戏。

在面向对象编程中,这是允许也是合法的!(除非像Java设置了final属性,这不在本文范畴)

那么,我修改一下:

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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello
"""
下面是一个程序员类定义
"""


class Programmer():
def code(self):
print("life is short, why not python?")


"""
下面是一个学生类定义
"""


class Student(object):
"""这里是一个学生类定义"""

def __init__(self, name):
self.name = name

def get_name(self):
return self.name

def set_name(self, name):
self.name = name

def study(self):
print(f"{self.name} : 好好学习,天天向上!")


class PrimarySchoolStudent(Student, Programmer):
def study(self):
print(f"{self.name} :开开心心玩游戏!")


print("*" * 16)
xiaopengyou = PrimarySchoolStudent("一个小学生(学委的一个粉丝)")
xiaopengyou.study()
xiaopengyou.code()

再次说一次,继承让子类直接拥有了父类的函数和数据属性,可以直接访问拿来用。

然后本次更改只是比前面第一段代码多了一个函数,学委在PrimarySchoolStudent,重新定义了一个study函数。

所以子类的函数把父类函数覆盖了,(父类也蒙圈中,他从来不知道这件事!)注意必须说同名的函数!

好了,看看下面的运行结果:

屏幕快照 2021-11-18 上午12.25.30.png

总结

类继承产生父子传承,但是允许父子存在差异。今天说到这。

学委写了十几年的Java了,但是写的这套Python教程非常务实,对基础编程有任何问题请查看相关文章。

喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!

编程很有趣,关键是把技术搞透彻讲明白。

欢迎关注微信,点赞支持收藏!

本文转载自: 掘金

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

1…301302303…956

开发者博客

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