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

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


  • 首页

  • 归档

  • 搜索

详解Spring Security的HttpBasic登录验

发表于 2019-11-15

【北京】 IT技术人员面对面试、跳槽、升职等问题,如何快速成长,获得大厂入门资格和升职加薪的筹码?与大厂技术大牛面对面交流,解答你的疑惑。《从职场小白到技术总监成长之路:我的职场焦虑与救赎》活动链接:码客

恭喜fpx,新王登基,lpl*b 我们是冠军

原文链接:segmentfault.com/a/119000002…

一、HttpBasic模式的应用场景

HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验证。

就好像是我小时候写日记,都买一个带小锁头的日记本,实际上这个小锁头有什么用呢?如果真正想看的人用一根钉子都能撬开。它的作用就是:某天你的父母想偷看你的日记,拿出来一看还带把锁,那就算了吧,怪麻烦的。

举一个我使用HttpBasic模式的进行登录验证的例子:我曾经在一个公司担任部门经理期间,开发了一套用于统计效率、分享知识、生成代码、导出报表的Http接口。纯粹是为了工作中提高效率,同时我又有一点点小私心,毕竟各部之间是有竞争的,所以我给这套接口加上了HttpBasic验证。公司里随便一个技术人员,最多只要给上一两个小时,就可以把这个验证破解了。说白了,这个工具的数据不那么重要,加一道锁的目的就是不让它成为公开数据。如果有心人破解了,真想看看这里面的数据,其实也无妨。这就是HttpBasic模式的典型应用场景。

二、spring boot2.0整合Spring security

spring boot 2,x版本maven方式引入Spring security坐标。

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

三、HttpBasic登录认证模式

如果使用的Spring Boot版本为1.X版本,依赖的Security 4.X版本,那么就无需任何配置,启动项目访问则会弹出默认的httpbasic认证.

我们现在使用的是spring boot2.0版本(依赖Security 5.X版本),HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单模式。所以我们要使用Basic模式,需要自己调整一下。并且security.basic.enabled已经过时了,所以我们需要自己去编码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()//开启httpbasic认证
.and()
.authorizeRequests()
.anyRequest()
.authenticated();//所有请求都需要登录认证才能访问
}

}

启动项目,在项目后台有这样的一串日志打印,冒号后面的就是默认密码。

1
复制代码Using generated security password: 0cc59a43-c2e7-4c21-a38c-0df8d1a6d624

我们可以通过浏览器进行登录验证,默认的用户名是user.(下面的登录框不是我们开发的,是HttpBasic模式自带的)

在这里插入图片描述

当然我们也可以通过application.yml指定配置用户名密码

1
2
3
4
5
复制代码spring:
security:
user:
name: admin
password: admin

四、HttpBasic模式的原理说明

file

  • 首先,HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 “admin“ ,密码是“ admin”,则将字符串”admin:admin“ 使用Base64编码算法加密。加密结果可能是:YWtaW46YWRtaW4=。
  • 然后,在Http请求中使用Authorization作为一个Header,“Basic YWtaW46YWRtaW4=“作为Header的值,发送给服务端。(注意这里使用Basic+空格+加密串)
  • 服务器在收到这样的请求时,到达BasicAuthenticationFilter过滤器,将提取“ Authorization”的Header值,并使用用于验证用户身份的相同算法Base64进行解码。
  • 解码结果与登录验证的用户名密码匹配,匹配成功则可以继续过滤器后续的访问。

所以,HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是可逆的,你知道上面的原理,分分钟就破解掉。我们完全可以使用PostMan工具,发送Http请求进行登录验证。

file

(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)

本文转载自: 掘金

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

浅析Memcached, Redis, MongoDB三者的

发表于 2019-11-15

Redis

是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

  • 查看Redis中文命令大全
  • Redis 负载监控——redis-monitor,一个 web 可视化的 redis 监控程序。
  • Redis 集群迁移工具 Redis-Migrate-Tool,基于redis复制,快速,稳定。
  • 优酷土豆的Redis服务平台化之路
  • Redis中国用户组|唯品会Redis cluster大规模生产实践

Redis的优点:

  1. 支持多种数据结构,如 string(字符串)、 list(双向链表)、dict(hash表)、set(集合)、zset(排序set)、hyperloglog(基数估算)。
  2. 支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段。
  3. 支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制,master-slave机制是Redis进行HA的重要手段。
  4. 单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题。
  5. 支持pub/sub消息订阅机制,可以用来进行消息订阅与通知。
  6. 支持简单的事务需求,但业界使用场景很少,并不成熟。

Redis的局限性:

  1. Redis只能使用单线程,性能受限于CPU性能,故单实例CPU最高才可能达到5-6wQPS每秒(取决于数据结构,数据大小以及服务器硬件性能,日常环境中QPS高峰大约在1-2w左右)。
    支持简单的事务需求,但业界使用场景很少,并不成熟,既是优点也是缺点。
  2. Redis在string类型上会消耗较多内存,可以使用dict(hash表)压缩存储以降低内存耗用。
  3. Mc和Redis都是Key-Value类型,不适合在不同数据集之间建立关系,也不适合进行查询搜索。比如redis的keys pattern这种匹配操作,对redis的性能是灾难。

upload successfulupload successful

Memcached

是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。Memcached基于一个存储键/值对的hashmap。其守护进程(daemon )是用C写的,但是客户端可以用任何语言来编写,并通过memcached协议与守护进程通信。

Memcached的优点:

  1. Memcached可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS(取决于key、value的字节大小以及服务器硬件性能,日常环境中QPS高峰大约在4-6w左右)。适用于最大程度扛量。
  2. 支持直接配置为session handle。

Memcached的局限性:

  1. 只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。
  2. 无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失。
  3. 无法进行数据同步,不能将MC中的数据迁移到其他MC实例中。
  4. Memcached内存分配采用Slab Allocation机制管理内存,value大小分布差异较大时会造成内存利用率降低,并引发低利用率时依然出现踢出等问题。需要用户注重value设计。

upload successfulupload successful

MongoDB

是一个基于分布式文件存储的数据库,文档型的非关系型数据库,与上面两者不同。

先解释一下文档的数据库,即可以存放xml、json、bson类型系那个的数据。

这些数据具备自述性(self-describing),呈现分层的树状数据结构。redis可以用hash存放简单关系型数据。

MongoDB存放json格式数据。

适合场景:事件记录、内容管理或者博客平台,比如评论系统。

upload successfulupload successful

Redis与Memcached的比较

1、数据类型支持不同

与Memcached仅支持简单的key-value结构的数据记录不同,Redis支持的数据类型要丰富得多。最为常用的数据类型主要由五种:String、Hash、List、Set和Sorted Set。Redis内部使用一个redisObject对象来表示所有的key和value。

2、内存管理机制不同

在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。

当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘。Redis只会缓存所有的key的信息,如果Redis发现内存的使用量超过了某一个阀值,将触发swap的操作,Redis根据“swappability = age*log(size_in_memory)”计算出哪些key对应的value需要swap到磁盘。然后再将这些key对应的value持久化到磁盘中,同时在内存中清除。

这种特性使得Redis可以保持超过其机器本身内存大小的数据。当然,机器本身的内存必须要能够保持所有的key,毕竟这些数据是不会进行swap操作的。同时由于Redis将内存中的数据swap到磁盘中的时候,提供服务的主线程和进行swap操作的子线程会共享这部分内存,所以如果更新需要swap的数据,Redis将阻塞这个操作,直到子线程完成swap操作后才可以进行修改。

当从Redis中读取数据的时候,如果读取的key对应的value不在内存中,那么Redis就需要从swap文件中加载相应数据,然后再返回给请求方。 这里就存在一个I/O线程池的问题。在默认的情况下,Redis会出现阻塞,即完成所有的swap文件加载后才会相应。这种策略在客户端的数量较小,进行批量操作的时候比较合适。但是如果将Redis应用在一个大型的网站应用程序中,这显然是无法满足大并发的情况的。所以Redis运行我们设置I/O线程池的大小,对需要从swap文件中加载相应数据的读取请求进行并发操作,减少阻塞的时间。

3、数据持久化支持

Redis虽然是基于内存的存储系统,但是它本身是支持内存数据的持久化的,而且提供两种主要的持久化策略:RDB快照和AOF日志。而memcached是不支持数据持久化操作的。

4、集群管理的不同

Memcached是全内存的数据缓冲系统,Redis虽然支持数据的持久化,但是全内存毕竟才是其高性能的本质。作为基于内存的存储系统来说,机器物理内存的大小就是系统能够容纳的最大数据量。如果需要处理的数据量超过了单台机器的物理内存大小,就需要构建分布式集群来扩展存储能力。

Memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。

结论

  • 没有必要过多的关心性能,因为二者的性能都已经足够高了。由于Redis只使用单核,而Memcached可以使用多核,所以在比较上,平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。说了这么多,结论是,无论你使用哪一个,每秒处理请求的次数都不会成为瓶颈。(比如瓶颈可能会在网卡)
  • 如果要说内存使用效率,使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。当然,这和你的应用场景和数据特性有关。
  • 如果你对数据持久化和数据同步有所要求,那么推荐你选择Redis,因为这两个特性Memcached都不具备。即使你只是希望在升级或者重启系统后缓存数据不会丢失,选择Redis也是明智的。
  • 当然,最后还得说到你的具体应用需求。Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果你需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。

本文转载自: 掘金

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

面试官:如何保证消息不被重复消费?或者说,如何保证消息消费的

发表于 2019-11-15

本文由 yanglbme 首发于 GitHub 技术社区 Doocs,目前 stars 已超 30k。
项目地址:github.com/doocs/advan…

stars

面试题

如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性?

面试官心理分析

其实这是很常见的一个问题,这俩问题基本可以连起来问。既然是消费消息,那肯定要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?这个是 MQ 领域的基本问题,其实本质上还是问你使用消息队列如何保证幂等性,这个是你架构里要考虑的一个问题。

面试题剖析

回答这个问题,首先你别听到重复消息这个事儿,就一无所知吧,你先大概说一说可能会有哪些重复消费的问题。

首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。

Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。

但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。

举个栗子。

有这么个场景。数据 1/2/3 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是 152/153/154。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 offset=153 的这条数据,刚准备去提交 offset 到 zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 offset 并没有提交,kafka 也就不知道你已经消费了 offset=153 这条数据。那么重启之后,消费者会找 kafka 说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。

如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2 次,那么数据就错啦。

其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性。

举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。

一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。

幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。

所以第二个问题来了,怎么保证消息队列消费的幂等性?

其实还是得结合业务来思考,我这里给几个思路:

  • 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
  • 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。


欢迎关注我的微信公众号“Doocs开源社区”,原创技术文章第一时间推送。

本文转载自: 掘金

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

《提升能力,涨薪可待》-Java并发之AQS全面详解

发表于 2019-11-15

欢迎关注掘金:【Ccww】,一起学习

提升能力,涨薪可待

面试知识,工作可待

实战演练,拒绝996

也欢迎关注微信公众号【Ccww技术博客】,原创技术文章第一时间推出

如果此文对你有帮助、喜欢的话,那就点个赞呗!

前言

是不是感觉在工作上难于晋升了呢?

是不是感觉找工作面试是那么难呢?

是不是感觉自己每天都在996加班呢?

在工作上必须保持学习的能力,这样才能在工作得到更好的晋升,涨薪指日可待,欢迎一起学习【提升能力,涨薪可待】系列

在找工作面试应在学习的基础进行总结面试知识点,工作也指日可待,欢迎一起学习【面试知识,工作可待】系列

最后,理论知识到准备充足,是不是该躬行起来呢?欢迎一起学习【实战演练,拒绝996】系列

一、AQS是什么?有什么用?

​ AQS全称AbstractQueuedSynchronizer,即抽象的队列同步器,是一种用来构建锁和同步器的框架。

基于AQS构建同步器:

  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • ReentrantReadWriteLock
  • SynchronusQueue
  • FutureTask

优势:

  • AQS 解决了在实现同步器时涉及的大量细节问题,例如自定义标准同步状态、FIFO 同步队列。
  • 基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。

二、AQS核心知识

2.1 AQS核心思想

  如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。如图所示:

Sync queue: 同步队列,是一个双向列表。包括head节点和tail节点。head节点主要用作后续的调度。

Condition queue: 非必须,单向列表。当程序中存在cindition的时候才会存在此列表。

2.2 AQS设计思想

  • AQS使用一个int成员变量来表示同步状态
  • 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置
  • AQS资源共享方式:独占Exclusive(排它锁模式)和共享Share(共享锁模式)

AQS它的所有子类中,要么实现并使用了它的独占功能的api,要么使用了共享锁的功能,而不会同时使用两套api,即便是最有名的子类ReentrantReadWriteLock也是通过两个内部类读锁和写锁分别实现了两套api来实现的

2.3 state状态

state状态使用volatile int类型的变量,表示当前同步状态。state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

2.4 AQS中Node常量含义

CANCELLED

waitStatus值为1时表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞。

SIGNAL

waitStatus为-1时表示该线程的后续线程需要阻塞,即只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程

CONDITION

waitStatus为-2时,表示该线程在condition队列中阻塞(Condition有使用)

PROPAGATE

waitStatus为-3时,表示该线程以及后续线程进行无条件传播(CountDownLatch中有使用)共享模式下, PROPAGATE 状态的线程处于可运行状态

2.5 同步队列为什么称为FIFO呢?

因为只有前驱节点是head节点的节点才能被首先唤醒去进行同步状态的获取。当该节点获取到同步状态时,它会清除自己的值,将自己作为head节点,以便唤醒下一个节点。

2.6 Condition队列

​ 除了同步队列之外,AQS中还存在Condition队列,这是一个单向队列。调用ConditionObject.await()方法,能够将当前线程封装成Node加入到Condition队列的末尾,然后将获取的同步状态释放(即修改同步状态的值,唤醒在同步队列中的线程)。

Condition队列也是FIFO。调用ConditionObject.signal()方法,能够唤醒firstWaiter节点,将其添加到同步队列末尾。

 

2.7 自定义同步器的实现

在构建自定义同步器时,只需要依赖AQS底层再实现共享资源state的获取与释放操作即可。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

三 AQS实现细节

线程首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到FIFO队列中。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。

3.1 独占模式下的AQS

​ 所谓独占模式,即只允许一个线程获取同步状态,当这个线程还没有释放同步状态时,其他线程是获取不了的,只能加入到同步队列,进行等待。

很明显,我们可以将state的初始值设为0,表示空闲。当一个线程获取到同步状态时,利用CAS操作让state加1,表示非空闲,那么其他线程就只能等待了。释放同步状态时,不需要CAS操作,因为独占模式下只有一个线程能获取到同步状态。ReentrantLock、CyclicBarrier正是基于此设计的。

例如,ReentrantLock,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。

​ 独占模式下的AQS是不响应中断的,指的是加入到同步队列中的线程,如果因为中断而被唤醒的话,不会立即返回,并且抛出InterruptedException。而是再次去判断其前驱节点是否为head节点,决定是否争抢同步状态。如果其前驱节点不是head节点或者争抢同步状态失败,那么再次挂起。

3.1.1 独占模式获取资源-acquire方法

acquire以独占exclusive方式获取资源。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。源码如下:

1
2
3
4
5
复制代码 public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

流程图:

  • 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  • 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  • acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  • 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

3.1.2 独占模式获取资源-tryAcquire方法

tryAcquire尝试以独占的方式获取资源,如果获取成功,则直接返回true,否则直接返回false,且具体实现由自定义AQS的同步器实现的。

1
2
3
复制代码 protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

3.1.3 独占模式获取资源-addWaiter方法

  根据不同模式(Node.EXCLUSIVE互斥模式、Node.SHARED共享模式)创建结点并以CAS的方式将当前线程节点加入到不为空的等待队列的末尾(通过compareAndSetTail()方法)。如果队列为空,通过enq(node)方法初始化一个等待队列,并返回当前节点。

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
复制代码/**
* 参数
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* 返回值
* @return the new node
*/
private Node addWaiter(Node mode) {
//将当前线程以指定的模式创建节点node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 获取当前同队列的尾节点
Node pred = tail;
//队列不为空,将新的node加入等待队列中
if (pred != null) {
node.prev = pred;
//CAS方式将当前节点尾插入队列中
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//当队列为empty或者CAS失败时会调用enq方法处理
enq(node);
return node;
}

其中,队列为empty,使用enq(node)处理,将当前节点插入等待队列,如果队列为空,则初始化当前队列。所有操作都是CAS自旋的方式进行,直到成功加入队尾为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码 private Node enq(final Node node) {
//不断自旋
for (;;) {
Node t = tail;
//当前队列为empty
if (t == null) { // Must initialize
//完成队列初始化操作,头结点中不放数据,只是作为起始标记,lazy-load,在第一次用的时候new
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//不断将当前节点使用CAS尾插入队列中直到成功为止
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

3.1.4 独占模式获取资源-acquireQueued方法

acquireQueued用于已在队列中的线程以独占且不间断模式获取state状态,直到获取锁后返回。主要流程:

  • 结点node进入队列尾部后,检查状态;
  • 调用park()进入waiting状态,等待unpark()或interrupt()唤醒;
  • 被唤醒后,是否获取到锁。如果获取到,head指向当前结点,并返回从入队到获取锁的整个过程中是否被中断过;如果没获取到,继续流程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
复制代码final boolean acquireQueued(final Node node, int arg) {
//是否已获取锁的标志,默认为true 即为尚未
boolean failed = true;
try {
//等待中是否被中断过的标记
boolean interrupted = false;
for (;;) {
//获取前节点
final Node p = node.predecessor();
//如果当前节点已经成为头结点,尝试获取锁(tryAcquire)成功,然后返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire根据对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作
//parkAndCheckInterrupt让线程进入等待状态,并检查当前线程是否被可以被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//将当前节点设置为取消状态;取消状态设置为1
if (failed)
cancelAcquire(node);
}
}

3.1.5 独占模式释放资源-release方法

release方法是独占exclusive模式下线程释放共享资源的锁。它会调用tryRelease()释放同步资源,如果全部释放了同步状态为空闲(即state=0),当同步状态为空闲时,它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock().

1
2
3
4
5
6
7
8
9
复制代码    public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

3.1.6 独占模式释放资源-tryRelease方法

tryRelease()跟tryAcquire()一样实现都是由自定义定时器以独占exclusive模式实现的。因为其是独占模式,不需要考虑线程安全的问题去释放共享资源,直接减掉相应量的资源即可(state-=arg)。而且tryRelease()的返回值代表着该线程是否已经完成资源的释放,因此在自定义同步器的tryRelease()时,需要明确这条件,当已经彻底释放资源(state=0),要返回true,否则返回false。

1
2
3
复制代码 protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

ReentrantReadWriteLock的实现:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//减掉相应量的资源(state-=arg)
int nextc = getState() - releases;
//是否完全释放资源
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

3.1.7 独占模式释放资源-unparkSuccessor

unparkSuccessor用unpark()唤醒等待队列中最前驱的那个未放弃线程,此线程并不一定是当前节点的next节点,而是下一个可以用来唤醒的线程,如果这个节点存在,调用unpark()方法唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码 private void unparkSuccessor(Node node) {
//当前线程所在的结点node
int ws = node.waitStatus;
//置零当前线程所在的结点状态,允许失败
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//找到下一个需要唤醒的结点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从后向前找
for (Node t = tail; t != null && t != node; t = t.prev)
//从这里可以看出,<=0的结点,都是还有效的结点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}

3.2 共享模式下的AQS

​ 共享模式,当然是允许多个线程同时获取到同步状态,共享模式下的AQS也是不响应中断的.

很明显,我们可以将state的初始值设为N(N > 0),表示空闲。每当一个线程获取到同步状态时,就利用CAS操作让state减1,直到减到0表示非空闲,其他线程就只能加入到同步队列,进行等待。释放同步状态时,需要CAS操作,因为共享模式下,有多个线程能获取到同步状态。CountDownLatch、Semaphore正是基于此设计的。

例如,CountDownLatch,任务分为N个子线程去执行,同步状态state也初始化为N(注意N要与线程个数一致):  

 

3.2.1 共享模式获取资源-acquireShared方法

acquireShared在共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

1
2
3
4
复制代码public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

流程:

  • 先通过tryAcquireShared()尝试获取资源,成功则直接返回;
  • 失败则通过doAcquireShared()中的park()进入等待队列,直到被unpark()/interrupt()并成功获取到资源才返回(整个等待过程也是忽略中断响应)。

3.2.2 共享模式获取资源-tryAcquireShared方法

tryAcquireShared()跟独占模式获取资源方法一样实现都是由自定义同步器去实现。但AQS规范中已定义好tryAcquireShared()的返回值:

  • 负值代表获取失败;
  • 0代表获取成功,但没有剩余资源;
  • 正数表示获取成功,还有剩余资源,其他线程还可以去获取。
1
2
3
复制代码 protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}

3.2.3 共享模式获取资源-doAcquireShared方法

doAcquireShared()用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。

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
复制代码private void doAcquireShared(int arg) {
//加入队列尾部
final Node node = addWaiter(Node.SHARED);
//是否成功标志
boolean failed = true;
try {
//等待过程中是否被中断过的标志
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取前驱节点
if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}

//判断状态,队列寻找一个适合位置,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

3.2.4 共享模式释放资源-releaseShared方法

releaseShared()用于共享模式下线程释放共享资源,释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

1
2
3
4
5
6
7
8
9
复制代码public final boolean releaseShared(int arg) {
//尝试释放资源
if (tryReleaseShared(arg)) {
//唤醒后继结点
doReleaseShared();
return true;
}
return false;
}

独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。

https://www.cnblogs.com/waterystone/p/4920797.html

3.2.共享模式释放资源-doReleaseShared方法

doReleaseShared()主要用于唤醒后继节点线程,当state为正数,去获取剩余共享资源;当state=0时去获取共享资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒后继
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// head发生变化
if (h == head)
break;
}
}

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!

欢迎关注公众号【Ccww技术博客】,原创技术文章第一时间推出

本文转载自: 掘金

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

Java中「与运算,或运算,异或运算,取反运算。」

发表于 2019-11-14

版权声明一:本文为博主原创文章,转载请附上原文出处链接和本声明。
版权声明二:本网站的所有作品会及时更新,欢迎大家阅读后发表评论,以利作品的完善。
版权声明三:对不遵守本声明或其他违法、恶意使用本网内容者,保留追究其法律责任的权利。

Java中的「与运算 & 」 规则 :都为1时才为1,否则为0

*** 即:两位同时为“1”,结果才为“1”,否则为0

例如:
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
ini复制代码    public static void main(String args[]) {
        System.out.println( 7 & 9);

        /*
         * 7的二进制
         * 7/2=3...1
         * 3/2=1...1
         * 1/2=0...1
         * 直到商为0,将余数倒过来就是111
         * 于是得数是111
         */

        /*
         * 9的二进制
         * 9/2=4...1
         * 4/2=2...0
         * 2/2=1...0
         * 1/2=0...1
         * 于是得数是1001
         */

        /*
         *  7二进制 0111    
         *  9二进制 1001
         * ------------ 
         *          0001   ==1
         */        
    }

Java中的「或运算 | 」 规则 :有一个为1,则为1

  • 即 :参加运算的两个对象只要有一个为1,其值为1。
例如:
1
2
3
4
5
6
7
8
9
10
typescript复制代码    public static void main(String args[]){     
        System.out.println(7 | 9);  

        /*
         * 7二进制 0111
         * 9二进制 1001
         * -----------
         *         1111 == 15
         * */    
    }

Java中的「异或运算 ^ 」 规则 :都不同时,为1

  • 即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
例如:
1
2
3
4
5
6
7
8
9
typescript复制代码    public static void main(String args[]){
        System.out.println( 7 ^ 9);
        /*
         * 7二进制 0111
         * 9二进制 1001
         * ------------
         *        1110 == 14
         * */
    }

Java中的「取反运算 ~ 」 规则 :按位取反

  • 即:对一个二进制数按位取反,即将1变0,0变1。
  • 按位取反运算符“~”的原理:是将内存中的补码按位取反(包括符号位)。
1. 二进制数在内存中是以补码的形式存放的。
2. 补码首位是符号位,0表示此数为正数,1表示此数为负数。
3. 正数的补码、反码,都是其本身。
4. 负数的反码是:符号位为1,其余各位求反,但末位不加1 。
5. 负数的补码是:符号位不变,其余各位求反,末位加1 。
6. 所有的取反操作、加1、减1操作,都在有效位进行。
+ 例如:正数
+ 正数9(二进制为:1001)在内存中存储为01001,必须补上符号位(开头的数字0为符号位)。
+ 转二进制:0 1001
+ 计算补码:0 1001
+ 按位取反:1 0110 (变成补码,这明显变成了一个负数补码,因为符号位是1)
+ 补码减1:1 0101
+ 在取反: 1 1010
+ 符号位为1是负数,即-10
+ 例如:负数
+ 负数-1(二进制为:0001)在内存中存储为10001,必须补上符号位(开头的数字1为符号位)。
+ -1的反码为11110
+ -1的补码为11111 (也可以理解为:反码末位加上1就是补码)
+ ~-1的取反 00000
+ ~-1结果为:0
例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码package test2;

public class CeshiQuFan {

    public static void main(String args[]){
            System.out.println(~7);//正数 

        /*
         * 7二进制 0000 0000 0000 0000 0000 0000 0000 0111
         *         0000 0000 0000 0000 0000 0000 0000 0111 反码
         *         0000 0000 0000 0000 0000 0000 0000 0111 补码        
         *         1000 0000 0000 0000 0000 0000 0000 1000 得到正数的补码之后进行取反 (这时得到的是负数)符号位为: 1
         *         所以~7的值为:-8
         */
    }
}
例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码package test2;

public class CeshiQuFan {

    public static void main(String args[]){
            System.out.println(~-1);//负数    

  /*
   * -1二进制 1000 0000 0000 0000 0000 0000 0000 0001
   *          1000 0000 0000 0000 0000 0000 0000 1110  反码(负数的反码是:符号位为1,其余各位求反,但末位不加1。)
   *          1000 0000 0000 0000 0000 0000 0000 1111  补码(负数的补码是:符号位不变,其余各位求反,末位加1 。)
   *          0000 0000 0000 0000 0000 0000 0000 0000  得到负数的补码之后进行取反 (这时得到的是正数) 符号位为: 0
   *           所以~-1的值为:0
   */

    }
}

更多请参考:
常用进制览表**

本文转载自: 掘金

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

Spring Boot 2X(十七):应用监控之 Spri

发表于 2019-11-14

Admin 简介

Spring Boot Admin 是 Spring Boot 应用程序运行状态监控和管理的后台界面。最新UI使用vue.js重写里。

Spring Boot Admin 为已注册的应用程序提供了丰富的监控运维功能。如下:

  • 显示健康状况
  • 显示应用运行时的详细信息,如:JVM 和内存指标等
  • 计数器和测量指标
  • 数据源度量
  • 缓存度量
  • 跟踪和下载日志文件
  • 查看 jvm 系统和环境属性
  • 一键管理loglevel
  • 管理执行 JMX-beans
  • 查看线程转储
  • 查看跟踪信息
  • Hystrix-Dashboard集成(2.X版本已删除集成)
  • 下载 heapdump
  • 状态更改通知(支持:电子邮件、Slack、Hipchat等)
  • 状态更改事件日志(非永久性)

更多可以通过考文档详细了解。

Admin 使用及配置

Spring Boot Admin 服务端

项目依赖

1
2
3
4
5
6
7
8
9
10
复制代码    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- admin-server -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.1.6</version>
</dependency>

配置启动 Admin Server

1
2
3
4
5
6
7
8
9
10
复制代码@SpringBootApplication
@EnableAutoConfiguration
@EnableAdminServer
public class SpringBootAdminApplication {

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

}

application.properties 配置

1
2
复制代码server.port=9000
spring.application.name=Spring Boot Admin Web

测试

启动项目,通过浏览器访问 http://127.0.0.1:9000

Spring Boot Admin 客户端

这里以上面是 Spring Boot Actuator 项目为例

项目依赖

1
2
3
4
5
6
复制代码<!-- admin-client -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.1.6</version>
</dependency>

application.properties 配置

1
2
3
4
复制代码#设置 Admin Server 地址
server.port=8080
spring.application.name=Spring Boot Actuator Demo
spring.boot.admin.client.url=http://127.0.0.1:9000

测试

启动项目,通过浏览器访问 http://127.0.0.1:9000,我们会看到 Spring Boot Admin 的管理界面中 applications 会显示相应的客户端应用,点击应用进入详细的监控界面。

Spring Boot Admin 配置属性

Spring Boot Admin Server 配置属性详解

属性 描述 默认值
spring.boot.admin.context-path 上下文路径在应为Admin Server的静态资产和API提供服务的路径的前面加上前缀。相对于Dispatcher-Servlet /
spring.boot.admin.monitor.status-interval 更新client端状态的时间间隔,单位是毫秒 10000
spring.boot.admin.monitor.status-lifetime client端状态的生命周期,该生命周期内不会更新client状态,单位是毫秒 10000
spring.boot.admin.monitor.connect-timeout 查询client端状态信息时的连接超时,单位是毫秒 2000
spring.boot.admin.monitor.read-timeout 查询client端状态信息时的读取超时时间,单位是毫秒 10000
spring.boot.admin.monitor.default-retries 失败请求的默认重试次数。Modyfing请求(PUT,POST,PATCH,DELETE)将永远不会重试 0
spring.boot.admin.monitor.retries.* 键值对,具有每个endpointId的重试次数。默认为默认重试。Modyfing请求(PUT,POST,PATCH,DELETE)将永远不会重试
spring.boot.admin.metadata-keys-to-sanitize 要被过滤掉的元数据(当与正则表达式相匹配时,这些数据会在输出的json数据中过滤掉) “.password”, “.secret”, “.\key”, “.token”, “.credentials.”, “.*vcap_services”
spring.boot.admin.probed-endpoints 要获取的client的端点信息 “health”, “env”, “metrics”, “httptrace:trace”, “threaddump:dump”, “jolokia”, “info”, “logfile”, “refresh”, “flyway”, “liquibase”, “heapdump”, “loggers”, “auditevents”
spring.boot.admin.instance-proxy.ignored-headers 向client发起请求时不会被转发的headers信息 “Cookie”, “Set-Cookie”, “Authorization”
spring.boot.admin.ui.public-url 用于在ui中构建基本href的基本URL 如果在反向代理后面运行(使用路径重写),则可用于进行正确的自我引用。如果省略了主机/端口,将从请求中推断出来
spring.boot.admin.ui.brand 导航栏中显示的品牌 <img src="assets/img/icon-spring-boot-admin.svg"><span>Spring Boot Admin</span>
spring.boot.admin.ui.title 页面标题 “Spring Boot Admin”
spring.boot.admin.ui.favicon 用作默认图标的图标,用于桌面通知的图标 “assets/img/favicon.png”
spring.boot.admin.ui.favicon-danger 当一项或多项服务关闭并用于桌面通知时,用作网站图标 “assets/img/favicon-danger.png”

Spring Boot Admin Client 配置属性详解

属性 描述 默认值
spring.boot.admin.client.enabled 启用Spring Boot Admin Client true
spring.boot.admin.client.url 要注册的server端的url地址。如果要同时在多个server端口注册,则用逗号分隔各个server端的url地址
spring.boot.admin.client.api-path 管理服务器上注册端点的Http路径 “instances”
spring.boot.admin.client.username 如果server端需要进行认证时,该属性用于配置用户名
spring.boot.admin.client.password 如果server端需要进行认证时,该属性用于配置密码
spring.boot.admin.client.period 重复注册的时间间隔(以毫秒为单位) 10000
spring.boot.admin.client.connect-timeout 连接注册的超时时间(以毫秒为单位) 5000
spring.boot.admin.client.read-timeout 读取注册超时(以毫秒为单位) 5000
spring.boot.admin.client.auto-registration 如果设置为true,则在应用程序准备就绪后会自动安排注册应用程序的定期任务 true
spring.boot.admin.client.auto-deregistration 当上下文关闭时,切换为在Spring Boot Admin服务器上启用自动解密。如果未设置该值,并且在检测到正在运行的CloudPlatform时,该功能处于活动状态 null
spring.boot.admin.client.register-once 如果设置为true,则客户端将仅向一台管理服务器注册(由定义spring.boot.admin.instance.url);如果该管理服务器出现故障,将自动向下一个管理服务器注册。如果为false,则会向所有管理服务器注册 true
spring.boot.admin.client.instance.health-url 要注册的health-url地址。如果可访问URL不同(例如Docker),则可以覆盖。在注册表中必须唯一 默认该属性值与management-url 以及endpoints.health.id有关。比如工程中该值为:healthUrl=http://127.0.0.1:8080/actuator/health,其中http://127.0.0.1:8080/actuator是management-url,health是endpoints.health.id
spring.boot.admin.client.instance.management-base-url 用于计算要注册的管理URL的基本URL。该路径是在运行时推断的,并附加到基本URL 默认该属性值与management.port, service-url 以及server.servlet-path有关,如工程中该值为http://127.0.0.1:8080,其中8080端口是配置的获取actuator信息的端口。127.0.0.1是设置的service-url值,如果没有设置service-url的话,则为配置的server.servlet-path值(项目的启动路径)
spring.boot.admin.client.instance.management-url 要注册的management-url。如果可访问的URL不同(例如Docker),则可以覆盖 默认该属性值与management-base-url 和 management.context-path两个属性值有关,如 managementUrl=http://127.0.0.1:8080/actuator,其中http://127.0.0.1:8080为management-base-url,/actuator是management.context-path
spring.boot.admin.client.instance.service-base-url 用于计算要注册的服务URL的基本URL。该路径是在运行时推断的,并附加到基本URL 默认该属性值与hostname, server.port有关,如http://127.0.0.1:8080,其中8080端口是配置的server.port。127.0.0.1是client所在服务器的hostname
spring.boot.admin.client.instance.service-url 要注册的服务网址。如果可访问的URL不同(例如Docker),则可以覆盖 默认值是基于service-base-url 和 server.context-path进行赋值
spring.boot.admin.client.instance.name 要注册的名称 默认值是配置的spring.application.name的值,如果没有配置该属性的话,默认值是spring-boot-application
spring.boot.admin.client.instance.prefer-ip 在猜测的网址中使用ip地址而不是主机名。如果设置了server.address/ management.address,它将被使用。否则,InetAddress.getLocalHost()将使用从返回的IP地址 false
spring.boot.admin.client.instance.metadata.* 要与此实例相关联的元数据键值对
spring.boot.admin.client.instance.metadata.tags.* 标记作为要与此实例相关联的键值对

示例代码

github

码云

文档参考

codecentric.github.io/spring-boot…

非特殊说明,本文版权归 朝雾轻寒 所有,转载请注明出处.

原文标题:Spring Boot 2.X(十七):应用监控之 Spring Boot Admin 使用及配置

原文地址:https://www.zwqh.top/article/info/26

如果文章对您有帮助,请扫码关注下我的公众号,文章持续更新中…

本文转载自: 掘金

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

面试官:为什么使用消息队列?消息队列有什么优点和缺点?Kaf

发表于 2019-11-13

本文由 yanglbme 首发于 GitHub 技术社区 Doocs,目前 stars 已超 30k。
项目地址:github.com/doocs/advan…

stars

面试题

  • 为什么使用消息队列?
  • 消息队列有什么优点和缺点?
  • Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别,以及适合哪些场景?

面试官心理分析

其实面试官主要是想看看:

  • 第一,你知不知道你们系统里为什么要用消息队列这个东西?

不少候选人,说自己项目里用了 Redis、MQ,但是其实他并不知道自己为什么要用这个东西。其实说白了,就是为了用而用,或者是别人设计的架构,他从头到尾都没思考过。

没有对自己的架构问过为什么的人,一定是平时没有思考的人,面试官对这类候选人印象通常很不好。因为面试官担心你进了团队之后只会木头木脑的干呆活儿,不会自己思考。

  • 第二,你既然用了消息队列这个东西,你知不知道用了有什么好处&坏处?

你要是没考虑过这个,那你盲目弄个 MQ 进系统里,后面出了问题你是不是就自己溜了给公司留坑?你要是没考虑过引入一个技术可能存在的弊端和风险,面试官把这类候选人招进来了,基本可能就是挖坑型选手。就怕你干 1 年挖一堆坑,自己跳槽了,给公司留下无穷后患。

  • 第三,既然你用了 MQ,可能是某一种 MQ,那么你当时做没做过调研?

你别傻乎乎的自己拍脑袋看个人喜好就瞎用了一个 MQ,比如 Kafka,甚至都从没调研过业界流行的 MQ 到底有哪几种。每一个 MQ 的优点和缺点是什么。每一个 MQ 没有绝对的好坏,但是就是看用在哪个场景可以扬长避短,利用其优势,规避其劣势。

如果是一个不考虑技术选型的候选人招进了团队,leader 交给他一个任务,去设计个什么系统,他在里面用一些技术,可能都没考虑过选型,最后选的技术可能并不一定合适,一样是留坑。

面试题剖析

1. 为什么使用消息队列?

其实就是问问你消息队列都有哪些使用场景,然后你项目里具体是什么场景,说说你在这个场景里用消息队列是什么?

面试官问你这个问题,期望的一个回答是说,你们公司有个什么业务场景,这个业务场景有个什么技术挑战,如果不用 MQ 可能会很麻烦,但是你现在用了 MQ 之后带给了你很多的好处。

先说一下消息队列常见的使用场景吧,其实场景有很多,但是比较核心的有 3 个:解耦、异步、削峰。

解耦

看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃……

在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊!

如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。

总结:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。

面试技巧:你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦。

异步

再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。

一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。

如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快!

削峰

每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。

一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。

但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。

如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。

这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。

2. 消息队列有什么优缺点?

优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。

缺点有以下几个:

系统可用性降低

系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用?

系统复杂度提高

硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。

一致性问题

A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。

3. Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?

特性 ActiveMQ RabbitMQ RocketMQ Kafka
单机吞吐量 万级,比 RocketMQ、Kafka 低一个数量级 同 ActiveMQ 10 万级,支撑高吞吐 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic 数量对吞吐量的影响 topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
时效性 ms 级 微秒级,这是 RabbitMQ 的一大特点,延迟最低 ms 级 延迟在 ms 级以内
可用性 高,基于主从架构实现高可用 同 ActiveMQ 非常高,分布式架构 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性 有较低的概率丢失数据 基本不丢 经过参数优化配置,可以做到 0 丢失 同 RocketMQ
功能支持 MQ 领域的功能极其完备 基于 erlang 开发,并发能力很强,性能极好,延时很低 MQ 功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用

综上,各种对比之后,有如下建议:

一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。


欢迎关注我的微信公众号“Doocs开源社区”,原创技术文章第一时间推送。

本文转载自: 掘金

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

《我们一起进大厂》系列-Redis常见面试题(带答案)

发表于 2019-11-13

你知道的越多,你不知道的越多

点赞再看,养成习惯

GitHub上已经开源github.com/JavaFamily,有一线大厂面试点脑图,欢迎Star和完善

絮叨

上一期因为是在双十一一直在熬夜的大环境下完成的,所以我自己觉得质量明显没之前的好,我这不一睡好就加班加点准备补偿大家,来点干货。(熬夜太容易感冒了,这次点个赞别白嫖了!)

顺带提一嘴,我把我准备写啥画了一个思维导图,以后总不能每篇都放个贼大的图吧,就开源到了我的GitHub,大家有兴趣可以去完善和Star。

这篇我就先放出来大家看看,感觉还是差点意思,等大家完善了。

回望过去

上一期吊打系列我们提到了Redis相关的一些知识,还没看的小伙伴可以回顾一下

  • 《吊打面试官》系列-Redis基础
  • 《吊打面试官》系列-缓存雪崩、击穿、穿透
  • 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
  • 《吊打面试官》系列-Redis终章-凛冬将至、FPX-新王登基

这期我就从缓存到一些常见的问题讲一下,有一些我是之前提到过的,不过可能大部分仔是第一次看,我就重复发一下。

缓存知识点

缓存有哪些类型?

缓存是高并发场景下提高热点数据访问性能的一个有效手段,在开发项目时会经常使用到。

缓存的类型分为:本地缓存、分布式缓存和多级缓存。

本地缓存:

本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。

本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

分布式缓存:

分布式缓存可以很好得解决这个问题。

分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。

多级缓存:

为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。

在目前的一线大厂中,这也是最常用的缓存方案,单考单一的缓存方案往往难以撑住很多高并发的场景。

淘汰策略

不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。

一般的剔除策略有 FIFO 淘汰最早数据、LRU 剔除最近最少使用、和 LFU 剔除最近使用频率最低的数据几种策略。

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

其实在大家熟悉的LinkedHashMap中也实现了Lru算法的,实现如下:

当容量超过100时,开始执行LRU策略:将最近最少未使用的 TimeoutInfoHolder 对象 evict 掉。

真实面试中会让你写LUR算法,你可别搞原始的那个,那真TM多,写不完的,你要么怼上面这个,要么怼下面这个,找一个数据结构实现下Java版本的LRU还是比较容易的,知道啥原理就好了。

Memcache

注意后面会把 Memcache 简称为 MC。

先来看看 MC 的特点:

  • MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
  • MC 功能简单,使用内存存储数据;
  • MC 的内存结构以及钙化问题我就不细说了,大家可以查看官网了解下;
  • MC 对缓存的数据可以设置失效期,过期后的数据会被清除;
  • 失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
  • 当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。

另外,使用 MC 有一些限制,这些限制在现在的互联网场景下很致命,成为大家选择Redis、MongoDB的重要原因:

  • key 不能超过 250 个字节;
  • value 不能超过 1M 字节;
  • key 的最大失效时间是 30 天;
  • 只支持 K-V 结构,不提供持久化和主从同步功能。

Redis

先简单说一下 Redis 的特点,方便和 MC 比较。

  • 与 MC 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
  • Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
  • 相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
  • Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。

详解 Redis

Redis 的知识点结构如下图所示。

功能

来看 Redis 提供的功能有哪些吧!

我们先看基础类型:

String:

String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。

这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。

但是真实的开发环境中,很多仔可能会把很多比较复杂的结构也统一转成String去存储使用,比如有的仔他就喜欢把对象或者List转换为JSONString进行存储,拿出来再反序列话啥的。

我在这里就不讨论这样做的对错了,但是我还是希望大家能在最合适的场景使用最合适的数据结构,对象找不到最合适的但是类型可以选最合适的嘛,之后别人接手你的代码一看这么规范,诶这小伙子有点东西呀,看到你啥都是用的String,垃圾!

好了这些都是题外话了,道理还是希望大家记在心里,习惯成自然嘛,小习惯成就你。

String的实际应用场景比较广泛的有:

  • 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  • 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。

Hash:

这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。

但是这个的场景其实还是多少单一了一些,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象。我自己使用的场景用得不是那么多。

List:

List 是有序列表,这个还是可以玩儿出很多花样的。

比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。

比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。

比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。

List本身就是我们在开发过程中比较常用的数据结构了,热点数据更不用说了。

  • 消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
  • 文章列表或者数据分页展示的应用。

比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

Set:

Set 是无序集合,会自动去重的那种。

直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。

可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧。

反正这些场景比较多,因为对比很快,操作也简单,两个查询一个Set搞定。

Sorted Set:

Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。

有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。

  • 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  • 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

微博热搜榜,就是有个后面的热度值,前面就是名称

高级用法:

Bitmap :

位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter);

HyperLogLog:

供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV;

Geospatial:

可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近的人?或者计算最优地图路径?

这三个其实也可以算作一种数据结构,不知道还有多少朋友记得,我在梦开始的地方,Redis基础中提到过,你如果只知道五种基础类型那只能拿60分,如果你能讲出高级用法,那就觉得你有点东西。

pub/sub:

功能是订阅发布功能,可以用作简单的消息队列。

Pipeline:

可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。

Lua:

Redis 支持提交 Lua 脚本来执行一系列的功能。

我在前电商老东家的时候,秒杀场景经常使用这个东西,讲道理有点香,利用他的原子性。

话说你们想看秒杀的设计么?我记得我面试好像每次都问啊,想看的直接点赞后评论秒杀吧。

事务:

最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。

持久化

Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。

RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。

AOF 对日志文件的写入操作使用的追加模式,有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。

细节的点大家去高可用这章看,特别是两者的优缺点,以及怎么抉择。

《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU

高可用

来看 Redis 的高可用。Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentine l哨兵来监控 Redis 主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof 到新主。

选主的策略简单来说有三个:

  • slave 的 priority 设置的越低,优先级越高;
  • 同等情况下,slave 复制的数据越多优先级越高;
  • 相同的条件下 runid 越小越容易被选中。

在 Redis 集群中,sentinel 也会进行多实例部署,sentinel 之间通过 Raft 协议来保证自身的高可用。

Redis Cluster 使用分片机制,在内部分为 16384 个 slot 插槽,分布在所有 master 节点上,每个 master 节点负责一部分 slot。数据操作时按 key 做 CRC16 来计算在哪个 slot,由哪个 master 进行处理。数据的冗余是通过 slave 节点来保障。

哨兵

哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。

为啥必须要三个实例呢?我们先看看两个哨兵会咋样。

master宕机了 s1和s2两个哨兵只要有一个认为你宕机了就切换了,并且会选举出一个哨兵去执行故障,但是这个时候也需要大多数哨兵都是运行的。

那这样有啥问题呢?M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下S2个裸屌了,没有哨兵去允许故障转移了,虽然另外一个机器上还有R1,但是故障转移就是不执行。

经典的哨兵集群是这样的:

M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。

暖男我,小的总结下哨兵组件的主要功能:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

主从

提到这个,就跟我前面提到的数据持久化的RDB和AOF有着比密切的关系了。

我先说下为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。

你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。

我发出来之后来自CSDN的网友:Jian_Shen_Zer 问了个问题:

主从同步的时候,新的slaver进来的时候用RDB,那之后的数据呢?有新的数据进入master怎么同步到slaver啊

敖丙答:笨,AOF嘛,增量的就像MySQL的Binlog一样,把日志增量同步给从服务就好了

key 失效机制

Redis 的 key 可以设置过期时间,过期后 Redis 采用主动和被动结合的失效机制,一个是和 MC 一样在访问时触发被动删除,另一种是定期的主动删除。

定期+惰性+内存淘汰

缓存常见问题

缓存更新方式

这是决定在使用缓存时就该考虑的问题。

缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。

当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。

这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。

但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。

数据不一致

第二个问题是数据不一致的问题,可以说只要使用缓存,就要考虑如何面对这个问题。缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。

解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。

缓存穿透

缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。

解决的办法如下。

  1. 对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
  2. 使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。

缓存击穿

缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。

解决这个问题有如下办法。

  1. 可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
  2. 使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
  3. 针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。

缓存雪崩

缓存雪崩,产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。

解决方法:

  1. 使用快速失败的熔断策略,减少 DB 瞬间压力;
  2. 使用主从模式和集群模式来尽量保证缓存服务的高可用。

实际场景中,这两种方法会结合使用。

老朋友都知道为啥我没有大篇幅介绍这个几个点了吧,我在之前的文章实在是写得太详细了,忍不住点赞那种,我这里就不做重复拷贝了。

  • 《吊打面试官》系列-Redis基础
  • 《吊打面试官》系列-缓存雪崩、击穿、穿透
  • 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
  • 《吊打面试官》系列-Redis终章凛冬将至、FPX新王登基

考点与加分项

拿笔记一下!

考点

面试的时候问你缓存,主要是考察缓存特性的理解,对 MC、Redis 的特点和使用方式的掌握。

  • 要知道缓存的使用场景,不同类型缓存的使用方式,例如:
    • 对 DB 热点数据进行缓存减少 DB 压力;对依赖的服务进行缓存,提高并发性能;
    • 单纯 K-V 缓存的场景可以使用 MC,而需要缓存 list、set 等特殊数据格式,可以使用 Redis;
    • 需要缓存一个用户最近播放视频的列表可以使用 Redis 的 list 来保存、需要计算排行榜数据时,可以使用 Redis 的 zset 结构来保存。
  • 要了解 MC 和 Redis 的常用命令,例如原子增减、对不同数据结构进行操作的命令等。
  • 了解 MC 和 Redis 在内存中的存储结构,这对评估使用容量会很有帮助。
  • 了解 MC 和 Redis 的数据失效方式和剔除策略,比如主动触发的定期剔除和被动触发延期剔除
  • 要理解 Redis 的持久化、主从同步与 Cluster 部署的原理,比如 RDB 和 AOF 的实现方式与区别。
  • 要知道缓存穿透、击穿、雪崩分别的异同点以及解决方案。
  • 不管你有没有电商经验我觉得你都应该知道秒杀的具体实现,以及细节点。
  • ……..

欢迎去GitHub补充

加分项

如果想要在面试中获得更好的表现,还应了解下面这些加分项。

  • 是要结合实际应用场景来介绍缓存的使用。例如调用后端服务接口获取信息时,可以使用本地+远程的多级缓存;对于动态排行榜类的场景可以考虑通过 Redis 的 Sorted set 来实现等等。
  • 最好你有过分布式缓存设计和使用经验,例如项目中在什么场景使用过 Redis,使用了什么数据结构,解决哪类的问题;使用 MC 时根据预估值大小调整 McSlab 分配参数等等。
  • 最好可以了解缓存使用中可能产生的问题。比如 Redis 是单线程处理请求,应尽量避免耗时较高的单个请求任务,防止相互影响;Redis 服务应避免和其他 CPU 密集型的进程部署在同一机器;或者禁用 Swap 内存交换,防止 Redis 的缓存数据交换到硬盘上,影响性能。再比如前面提到的 MC 钙化问题等等。
  • 要了解 Redis 的典型应用场景,例如,使用 Redis 来实现分布式锁;使用 Bitmap 来实现 BloomFilter,使用 HyperLogLog 来进行 UV 统计等等。
  • 知道 Redis4.0、5.0 中的新特性,例如支持多播的可持久化消息队列 Stream;通过 Module 系统来进行定制功能扩展等等。
  • ……..

还是那句话欢迎去GitHub补充。

总结

这次是对我Redis系列的总结,这应该是Redis相关的最后一篇文章了,其实四篇看下来的小伙伴很多都从一知半解到了一脸懵逼,哈哈开个玩笑。

我觉得我的方式应该还好,大部分小伙伴还是比较能理解的,这篇之后我就不会写Redis相关的文章了(秒杀看大家想看的热度吧),有啥问题可以微信找我,下个系列写啥?

大家不用急,下个系列前我会发个有意思的文章,是我在公司代码创意大赛拿奖的文章,我觉得还是有点东西,我忍不住分享一下,顺便就在那期发起投票吧哈哈。

我看到很多小伙伴都有评论说想看别的,大概搜集了一下,还没留言的这期赶紧哟:

掘金

愚辛 :想看计算机基础,网络和操作系统那些(FPX牛脾)

cherish君:讲讲dubbo经常遇到的面试题目,太多人喜欢问dubbo😃

Java架构养成记:真的很香啊,下一期讲Dubbbo(重点SPI)然后讲MQ好吗

CSDN

小殿下:看完了所有的redis篇 希望可以出ssm

博客园

程然:Dubbo Dubbo

开源中国

linshi2019:这期明显是赶工之作啊

敖丙:这条我回一下,鞭策我,我很喜欢,不过说实话还是希望大家理解下,我双十一熬夜三天了,现在给你们写的时候也是值班回家2点左右了,我一天吃饭工作时间肯定是固定的,想写点东西就只有挤出睡觉时间了,这种产出肯定没周末全情投入写的来的质量高。

其实第一期看过来的小伙伴应该也知道,我在排版,还有很多文案,配图其实我一直都有在改进的,光是名词高亮我都要弄很久,因为怕大家看单一的黑白色调枯燥。

我是真的用心在搞,还是希望大家支持下理解下。

知乎、简书、思否、慕课手记没人看不知道为啥,懂行的老铁可以跟我说一下。

我只想说你们想看的肯定都在我开头和GITHub那个图里吧,问题不大,后面都会写的。

鸣谢

最后感谢下,新浪微博的技术专家张雷。

他于2013年加入新浪微博,作为核心技术人员参与了微博服务化、混合云等多个重点项目,是微博开源的RPC框架Motan的技术负责人,同时也负责微博的Service Mesh方案的研发与推广,专注于高可用架构及服务中间件开发方向。

他负责的Motan框架每天承载着万亿级别的请求调用,是微博平台服务化的基石,每次的突发热点事件、每次的春晚流量高峰,都离不开Motan框架的支撑与保障。此外,他也多次应邀在ArchSummit、WOT、GIAC技术峰会做技术分享。

感谢他对文章部分文案提供的支持和思路。

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。

我后面会每周都更新几篇一线互联网大厂面试和常用技术栈相关的文章,非常感谢人才们能看到这里,如果这个文章写得还不错,觉得「敖丙」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!

白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

敖丙 | 文 【原创】

如果本篇博客有任何错误,请批评指教,不胜感激 !


文章每周持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/JavaFamily 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

本文转载自: 掘金

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

用Java实现简单的区块链 用 Java 实现简单的区块链

发表于 2019-11-13

用 Java 实现简单的区块链

  1. 概述

本文中,我们将学习区块链技术的基本概念。也将根据概念使用 Java 来实现一个基本的应用程序。

进一步,我们将讨论一些先进的概念以及该技术的实际应用。

  1. 什么是区块链?

因此,让我们首先了解到底什么是区块链…

它的起源可以追溯到2008年 Satoshi Nakamoto 在比特币上发布的白皮书。

区块链是一个分散的信息分类账。它由通过使用密码学连接的数据块组成。它属于通过公共网络连接的节点网络。当我们稍后尝试构建一个基本教程时,我们会更好地理解这一点。

有一些我们必须要明白的重要属性,所以让我们来看看它们:

  • Tamper-proof [ 加密摘要 ]:首先也是最重要的,**数据作为块的一部分是防篡改的。**每个块都由加密摘要引用,通常称为哈希,使块防篡改。
  • Decentralized [ 分散化 ]:**整个区块链是完全分散**在网络上的。这意味着没有主节点,网络中的每个节点都有相同的副本。
  • Transparent [ 透明的,显而易见的 ]:每个参与网络的节点都**通过与其他节点的协商一致来验证并向其链添加一个新块**。因此,每个节点都具有完整的数据可视性。
  1. 区块链如何工作?

现在,让我们了解区块链如何工作。

区块链的基本单位是块。一个块能封装多个事务或者其它有价值的数据:

img

我们用哈希值表示一个块。生成块的哈希值叫做“挖掘”块。挖掘块通常在计算上很昂贵,因为它可以作为“工作证明”。

块的哈希值通常由以下数据组成:

  • 首先,块的哈希值由封装的事务组成。
  • 哈希也由块创建的时间戳组成
  • 它还包括一个 nonce,一个在密码学中使用的任意数字
  • 最后,当前块的哈希也包括前一个块的哈希

网络中的多个节点可以同时对数据块进行挖掘。除了生成哈希外,节点还必须验证添加到块中的事务是否合法。先挖一个街区,就赢了比赛!

3.2. 添加块到区块链

当挖掘一个块在计算上很昂贵时,验证块是否合法相对来说十分简单。所有在网络上的节点都参与验证新挖掘的块。

img

因此,在节点协商一致时将新挖掘的块添加到区块链中。

现在,我们可以使用几种共识协议进行验证。网络中的节点使用相同的协议来检测链的恶意分支。因此,即使引入了恶意分支,大多数节点也会很快拒绝它。

  1. Java 中的基本区块链

现在我们已经有了足够的上下文来开始用 Java 构建一个基本的应用程序。

我们这里的简单示例将演示我们刚才看到的基本概念。生产级应用程序包含许多超出本教程范围的考虑因素。不过,我们稍后将讨论一些高级主题。

4.1. 实现块

首先,我们需要定义一个简单的 POJO 来保存块数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class Block {
private String hash;
private String previousHash;
private String data;
private long timeStamp;
private int nonce;

public Block(String data, String previousHash, long timeStamp) {
this.data = data;
this.previousHash = previousHash;
this.timeStamp = timeStamp;
this.hash = calculateBlockHash();
}
// standard getters and setters
}

让我们来看看我们在这里打包了什么:

  • 前一个块的哈希,构建链的重要部分
  • 实际数据,任何有价值的信息,如合同
  • 块创建的时间戳
  • nonce,是密码学中使用的任意数字
  • 最后,块的哈希,根据其它数据计算

4.2. 计算哈希

现在,我们如何计算块的哈希?我们使用方法 calculateBlockHash ,但是还没有看到实现。在实现这个方法之前,值得花时间了解一下哈希是什么。

哈希是哈希函数的输出。哈希函数将任意大小的输入数据映射到固定大小的输出数据。哈希对输入数据中的任何更改都非常敏感,不管这些更改有多小。

此外,仅从它的哈希中获取输入数据是不可能的。这些属性使得哈希函数在密码学中非常有用。

那么,让我们看看如何在 Java 中生成块的哈希:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public String calculateBlockHash() {
String dataToHash = previousHash
+ Long.toString(timeStamp)
+ Integer.toString(nonce)
+ data;
MessageDigest digest = null;
byte[] bytes = null;
try {
digest = MessageDigest.getInstance("SHA-256");
bytes = digest.digest(dataToHash.getBytes(UTF_8));
} catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
logger.log(Level.SEVERE, ex.getMessage());
}
StringBuffer buffer = new StringBuffer();
for (byte b : bytes) {
buffer.append(String.format("%02x", b));
}
return buffer.toString();
}

这里发生了很多事情,让我们来详细了解一下:

  • 首先,我们将块的不同部分连接起来,生成一个哈希
  • 然后,我们从 MessageDigest 中获取 SHA-256 哈希函数的一个实例
  • 然后,我们生成输入数据的哈希值,它是一个字节数组
  • 最后,我们将字节数组转换为十六进制字符串,哈希通常表示为32位十六进制数字

4.3. 我们挖好了吗?

到目前为止,一切听起来都很简单和优雅,除了我们还没有挖掘过块。 那么究竟需要挖掘一个块,这已经吸引了开发人员一段时间的幻想!

因此,挖掘一个块意味着为块解决一个计算上复杂的任务。虽然计算块的哈希值比较简单,但是找到以5个0开头的哈希值就不那么简单了。更复杂的是找到一个以10个0开头的哈希,我们得到了一个大致的概念。

那么,我们到底该怎么做呢?老实说,这个解决方案没有想象中的那么好!我们是用蛮力来达到这个目标的。我们在这里使用 nonce:

1
2
3
4
5
6
7
8
复制代码public String mineBlock(int prefix) {
String prefixString = new String(new char[prefix]).replace('\0', '0');
while (!hash.substring(0, prefix).equals(prefixString)) {
nonce++;
hash = calculateBlockHash();
}
return hash;
}

让我们看看我们要做的是:

  • 我们首先定义要查找的前缀
  • 然后我们检查是否找到了答案
  • 如果没有,则增加 nonce 并在循环中计算哈希
  • 循环一直持续到我们中头奖

我们从 nonce 的默认值开始,并将其递增1。但是,在实际应用程序中,有更多复杂的策略来启动和增加 nonce。此外,我们没有验证我们的数据,这通常是一个重要的部分。

4.4. 运行示例

现在我们已经定义了块及其函数,我们可以使用它来创建一个简单的区块链。我们将它存储在一个 ArrayList 中:

1
2
3
复制代码List<Block> blockchain = new ArrayList<>();
int prefix = 4;
String prefixString = new String(new char[prefix]).replace('\0', '0');

此外,我们定义了一个前缀为4,这实际上意味着我们希望哈希以4个零开始。

让我们看看如何在这里添加一个块:

1
2
3
4
5
6
7
8
9
10
复制代码@Test
public void givenBlockchain_whenNewBlockAdded_thenSuccess() {
Block newBlock = new Block(
"The is a New Block.",
blockchain.get(blockchain.size() - 1).getHash(),
new Date().getTime());
newBlock.mineBlock(prefix);
assertTrue(newBlock.getHash().substring(0, prefix).equals(prefixString));
blockchain.add(newBlock);
}

4.5. 区块链验证

节点如何验证区块链是否有效?虽然这可能相当复杂,但让我们考虑一个简单的版本:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Test
public void givenBlockchain_whenValidated_thenSuccess() {
boolean flag = true;
for (int i = 0; i < blockchain.size(); i++) {
String previousHash = i==0 ? "0" : blockchain.get(i - 1).getHash();
flag = blockchain.get(i).getHash().equals(blockchain.get(i).calculateBlockHash())
&& previousHash.equals(blockchain.get(i).getPreviousHash())
&& blockchain.get(i).getHash().substring(0, prefix).equals(prefixString);
if (!flag) break;
}
assertTrue(flag);
}

所以,这里我们对每个块进行三次特定检查:

  • 存储的当前块的哈希实际上是它计算的内容
  • 当前块中存储的前一个块的哈希是前一个块的哈希
  • 当前区块已被开采
  1. 一些先进的概念

虽然我们的基本示例展示了区块链的基本概念,但它肯定不完整。要将这项技术投入实际应用,还需要考虑其他几个因素。

虽然不可能全部详细说明,但让我们来看看其中一些重要的:

5.1. 事务验证

计算块的哈希并找到所需的哈希仅仅只是挖掘的一部分。块由数据组成,通常以多个事务的形式存在。在成为块体的一部分并进行开采之前,必须对其进行验证。

区块链的一个典型实现是对一个块中可以包含多少数据做了限制。它还设置了如何验证事务的规则。网络中的多个节点参与验证过程。

5.2. 备用共识协议

我们看到的一致性算法如“工作证明”,被用来挖掘和验证块。但是,这并不是唯一可用的一致性算法。

还有几种其它一致性算法以供选择,如股权证明、权威证明和权重证明。所有这些都有其优缺点。使用哪一个取决于我们打算设计的应用程序的类型。

5.3. 挖掘报酬

区块链网络通常由自愿节点组成。 现在,为什么有人想要为这个复杂的过程做出贡献并保持其合法性并不断增长?

这是因为节点因验证事务和挖掘块而获得奖励。这些奖励通常以硬币的形式与应用程序相关联。但是应用程序可以决定奖励是任何有价值的东西。

5.4. 节点类型

区块链完全依赖于网络来进行操作。理论上,网络是完全分散的,每一个节点都是相等的。然而,在实践中,网络由多种类型的节点组成。

虽然完整节点具有完整的事务列表,但轻型节点仅具有部分列表。此外,不是所有的节点都参与验证和确认。

5.5. 安全通信

区块链技术的标志之一是其开放性和匿名性。 但它如何为内部交易提供安全保障? 这基于加密和公钥基础结构。

事务创始人使用私钥来保护它并将其附加到收件人的公钥。节点可以使用参与验证事务的公钥。

  1. 区块链的实际应用

因此,区块链似乎是一项令人兴奋的技术,但它也必须证明是有用的。这项技术已经存在一段时间了,不用说,它已经在许多领域被证明是具有破坏性的。

它在许多其他领域的应用正在积极进行。让我们了解最流行的应用程序:

  • 货币:由于比特币的成功,这是迄今为止最古老、最广为人知的区块链应用。它们向全球人民提供安全、无摩擦的资金,不需要任何中央政府或政府干预。
  • 身份:数字身份正迅速成为当今世界的常态。 但是,这会因安全问题和篡改而陷入困境。 区块链在彻底改变这一领域是不可避免的,具有完全安全和防篡改的身份。
  • 医疗保健:医疗保健行业充斥着数据,大多由中央政府处理。这会降低处理此类数据的透明度、安全性和效率。区块链技术可以提供一个没有任何第三方提供急需信任的系统。
  • 政府:这或许是一个很容易被区块链技术破坏的领域。区块链能够建立更好的政府与公民的关系。政府通常是几个公民服务机构的中心,这些机构往往充斥着低效和腐败。
  1. 行业工具

虽然我们这里的基本实现有助于引出概念,但是从头开始在区块链上开发产品是不现实的。值得庆幸的是,这个领域现在已经成熟了,我们确实有一些非常有用的工具可以开始使用。

让我们来看一些在这个领域工作的流行工具:

  • Solidity:Solidity 是一种静态类型和面向对象的编程语言,专为编写智能合约而设计。 它可以用来在像 Ethereum 这样的各种区块链平台上编写智能合约。
  • Remix IDE:Remix 是一个使用 solidity 编写智能合约的强大开源工具。这使用户可以直接从浏览器编写智能合约。
  • Truffle Suite:Truffle 提供了大量工具来帮助开发人员开始开发分布式应用程序。
  • Ethlint/Solium:Solium 允许开发人员确保他们写在 Solidity 上的智能合约没有风格和安全问题。同时,Solium 也有助于处理这些问题。
  • Parity:Parity 有助于设置在 Etherium 上智能合约的开发环境。它提供一种快速及有效的方法与区块链进行交互。
  1. 结论

总而言之,本节中,我们了解了区块链技术的基本概念。我们了解网络如何挖掘并在区块链中添加新区块。此外,我们用 Java 来实现了基本概念。我们还讨论了一些与之相关的先进概念。

最后,我们总结了区块链的一些实际应用以及可用的工具。

一如既往,代码可以在 GitHub 上找到。

原文:https://www.baeldung.com/java-blockchain

作者: Kumar Chandrakant

译者:Queena

9月福利,关注公众号​后台回复:004,领取8月翻译集锦!​往期福利回复:001,002, 003即可领取!

img

本文转载自: 掘金

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

Dubbo源码解析(八)远程通信——开篇 远程通讯——开篇

发表于 2019-11-12

远程通讯——开篇

目标:介绍之后解读远程通讯模块的内容如何编排、介绍dubbo-remoting-api中的包结构设计以及最外层的的源码解析。

前言

服务治理框架中可以大致分为服务通信和服务管理两个部分,前面我先讲到有关注册中心的内容,也就是服务管理,当然dubbo的服务管理还包括监控中心、 telnet 命令,它们起到的是人工的服务管理作用,这个后续再介绍。接下来我要讲解的就是跟服务通信有关的部分,也就是远程通讯模块。我在《dubbo源码解析(一)Hello,Dubbo》的”(六)dubbo-remoting——远程通信模块“中提到过一些内容。该模块中提供了多种客户端和服务端通信的功能,而在对NIO框架选型上,dubbo交由用户选择,它集成了mina、netty、grizzly等各类NIO框架来搭建NIO服务器和客户端,并且利用dubbo的SPI扩展机制可以让用户自定义选择。如果对SPI不太了解的朋友可以查看《dubbo源码解析(二)Dubbo扩展机制SPI》。

接下来我们先来看看dubbo-remoting的包结构:

remoting目录

我接下来解读远程通讯模块的内容并不是按照一个包一篇文章的编排,先来看看dubbo-remoting-api的包结构:

dubbo-remoting-api

可以看到,大篇幅的逻辑在dubbo-remoting-api中,所以我对于dubbo-remoting-api的解读会分为下面五个部分来说明,其中第五点会在本文介绍,其他四点会分别用四篇文章来介绍:

  1. buffer包:缓冲在NIO框架中是很重要的存在,各个NIO框架都实现了自己相应的缓存操作。这个buffer包下包括了缓冲区的接口以及抽象
  2. exchange包:信息交换层,其中封装了请求响应模式,在传输层之上重新封装了 Request-Response 语义,为了满足RPC的需求。这层可以认为专注在Request和Response携带的信息上。该层是RPC调用的通讯基础之一。
  3. telnet包:dubbo支持通过telnet命令来进行服务治理,该包下就封装了这些通用指令的逻辑实现。
  4. transport包:网络传输层,它只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输。该层是RPC调用的通讯基础之一。
  5. 最外层的源码:该部分我会在下面之间给出介绍。

为什么我要把一个api分成这么多文章来讲解,我们先来看看下面的图:

dubbo-framework

我们可以看到红框内的是远程通讯的框架,序列化我会在后面的主题中介绍,而Exchange层和Transport层在框架设计中起到了很重要的作用,也是支撑Remoting的核心,所以我要分开来介绍。

除了上述的五点外,根据惯例,我还是会分别介绍dubbo支持的实现客户端和服务端通信的七种方案,也就是说该远程通讯模块我会用12篇文章详细的讲解。

最外层源码解析

(一)接口Endpoint

dubbo抽象出一个端的概念,也就是Endpoint接口,这个端就是一个点,而点对点之间是可以双向传输。在端的基础上在衍生出通道、客户端以及服务端的概念,也就是下面要介绍的Channel、Client、Server三个接口。在传输层,其实Client和Server的区别只是在语义上区别,并不区分请求和应答职责,在交换层客户端和服务端也是一个点,但是已经是有方向的点,所以区分了明确的请求和应答职责。两者都具备发送的能力,只是客户端和服务端所关注的事情不一样,这个在后面会分开介绍,而Endpoint接口抽象的方法就是它们共同拥有的方法。这也就是它们都能被抽象成端的原因。

来看一下它的源码:

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
复制代码public interface Endpoint {

// 获得该端的url
URL getUrl();

// 获得该端的通道处理器
ChannelHandler getChannelHandler();

// 获得该端的本地地址
InetSocketAddress getLocalAddress();

// 发送消息
void send(Object message) throws RemotingException;

// 发送消息,sent是是否已经发送的标记
void send(Object message, boolean sent) throws RemotingException;

// 关闭
void close();

// 优雅的关闭,也就是加入了等待时间
void close(int timeout);

// 开始关闭
void startClose();

// 判断是否已经关闭
boolean isClosed();

}
  1. 前三个方法是获得该端本身的一些属性,
  2. 两个send方法是发送消息,其中第二个方法多了一个sent的参数,为了区分是否是第一次发送消息。
  3. 后面几个方法是提供了关闭通道的操作以及判断通道是否关闭的操作。

(二)接口Channel

该接口是通道接口,通道是通讯的载体。还是用自动贩卖机的例子,自动贩卖机就好比是一个通道,消息发送端会往通道输入消息,而接收端会从通道读消息。并且接收端发现通道没有消息,就去做其他事情了,不会造成阻塞。所以channel可以读也可以写,并且可以异步读写。channel是client和server的传输桥梁。channel和client是一一对应的,也就是一个client对应一个channel,但是channel和server是多对一对关系,也就是一个server可以对应多个channel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public interface Channel extends Endpoint {

// 获得远程地址
InetSocketAddress getRemoteAddress();

// 判断通道是否连接
boolean isConnected();

// 判断是否有该key的值
boolean hasAttribute(String key);

// 获得该key对应的值
Object getAttribute(String key);

// 添加属性
void setAttribute(String key, Object value);

// 移除属性
void removeAttribute(String key);

}

可以看到Channel继承了Endpoint,也就是端抽象出来的方法也同样是channel所需要的。上面的几个方法很好理解,我就不多介绍了。

(三)接口ChannelHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@SPI
public interface ChannelHandler {

// 连接该通道
void connected(Channel channel) throws RemotingException;

// 断开该通道
void disconnected(Channel channel) throws RemotingException;

// 发送给这个通道消息
void sent(Channel channel, Object message) throws RemotingException;

// 从这个通道内接收消息
void received(Channel channel, Object message) throws RemotingException;

// 从这个通道内捕获异常
void caught(Channel channel, Throwable exception) throws RemotingException;

}

该接口是负责channel中的逻辑处理,并且可以看到这个接口有注解@SPI,是个可扩展接口,到时候都会在下面介绍各类NIO框架的时候会具体讲到它的实现类。

(四)接口Client

1
2
3
4
5
6
7
8
9
10
复制代码public interface Client extends Endpoint, Channel, Resetable {

// 重连
void reconnect() throws RemotingException;

// 重置,不推荐使用
@Deprecated
void reset(com.alibaba.dubbo.common.Parameters parameters);

}

客户端接口,可以看到它继承了Endpoint、Channel和Resetable接口,继承Endpoint的原因上面我已经提到过了,客户端和服务端其实只是语义上的不同,客户端就是一个点。继承Channel是因为客户端跟通道是一一对应的,所以做了这样的设计,还继承了Resetable接口是为了实现reset方法,该方法,不过已经打上@Deprecated注解,不推荐使用。除了这些客户端就只需要关注一个重连的操作。

这里插播一个公共模块下的接口Resetable:

1
2
3
4
5
6
复制代码public interface Resetable {

// 用于根据新传入的 url 属性,重置自己内部的一些属性
void reset(URL url);

}

该方法就是根据新的url来重置内部的属性。

(五)接口Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public interface Server extends Endpoint, Resetable {

// 判断是否绑定到本地端口,也就是该服务器是否启动成功,能够连接、接收消息,提供服务。
boolean isBound();

// 获得连接该服务器的通道
Collection<Channel> getChannels();

// 通过远程地址获得该地址对应的通道
Channel getChannel(InetSocketAddress remoteAddress);

@Deprecated
void reset(com.alibaba.dubbo.common.Parameters parameters);

}

该接口是服务端接口,继承了Endpoint和Resetable,继承Endpoint是因为服务端也是一个点,继承Resetable接口是为了继承reset方法。除了这些以外,服务端独有的是检测是否启动成功,还有事获得连接该服务器上所有通道,这里获得所有通道其实就意味着获得了所有连接该服务器的客户端,因为客户端和通道是一一对应的。

(六)接口Codec && Codec2

这两个都是编解码器,那么什么叫做编解码器,在网络中只是讲数据看成是原始的字节序列,但是我们的应用程序会把这些字节组织成有意义的信息,那么网络字节流和数据间的转化就是很常见的任务。而编码器是讲应用程序的数据转化为网络格式,解码器则是讲网络格式转化为应用程序,同时具备这两种功能的单一组件就叫编解码器。在dubbo中Codec是老的编解码器接口,而Codec2是新的编解码器接口,并且dubbo已经用CodecAdapter把Codec适配成Codec2了。所以在这里我就介绍Codec2接口,毕竟人总要往前看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@SPI
public interface Codec2 {
//编码
@Adaptive({Constants.CODEC_KEY})
void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException;
//解码
@Adaptive({Constants.CODEC_KEY})
Object decode(Channel channel, ChannelBuffer buffer) throws IOException;

enum DecodeResult {
// 需要更多输入和忽略一些输入
NEED_MORE_INPUT, SKIP_SOME_INPUT
}

}

因为是编解码器,所以有两个方法分别是编码和解码,上述有以下几个关注的:

  1. Codec2是一个可扩展的接口,因为有@SPI注解。
  2. 用到了Adaptive机制,首先去url中寻找key为codec的value,来加载url携带的配置中指定的codec的实现。
  3. 该接口中有个枚举类型DecodeResult,因为解码过程中,需要解决 TCP 拆包、粘包的场景,所以增加了这两种解码结果,关于TCP 拆包、粘包的场景我就不多解释,不懂得朋友可以google一下。

(七)接口Decodeable

1
2
3
4
5
6
复制代码public interface Decodeable {

//解码
public void decode() throws Exception;

}

该接口是可解码的接口,该接口有两个作用,第一个是在调用真正的decode方法实现的时候会有一些校验,判断是否可以解码,并且对解码失败会有一些消息设置;第二个是被用来message核对用的。后面看具体的实现会更了解该接口的作用。

(八)接口Dispatcher

1
2
3
4
5
6
7
8
9
复制代码@SPI(AllDispatcher.NAME)
public interface Dispatcher {

// 调度
@Adaptive({Constants.DISPATCHER_KEY, "dispather", "channel.handler"})
// The last two parameters are reserved for compatibility with the old configuration
ChannelHandler dispatch(ChannelHandler handler, URL url);

}

该接口是调度器接口,dispatch是线程池的调度方法,这边有几个注意点:

  1. 该接口是一个可扩展接口,并且默认实现AllDispatcher,也就是所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。
  2. 用了Adaptive注解,也就是按照URL中配置来加载实现类,后面两个参数是为了兼容老版本,如果这是三个key对应的值都为空,就选择AllDispatcher来实现。

(九)接口Transporter

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@SPI("netty")
public interface Transporter {

// 绑定一个服务器
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
Server bind(URL url, ChannelHandler handler) throws RemotingException;

// 连接一个服务器,即创建一个客户端
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

该接口是网络传输接口,有以下几个注意点:

  1. 该接口是一个可扩展的接口,并且默认实现NettyTransporter。
  2. 用了dubbo SPI扩展机制中的Adaptive注解,加载对应的bind方法,使用url携带的server或者transporter属性值,加载对应的connect方法,使用url携带的client或者transporter属性值,不了解SPI扩展机制的可以查看《dubbo源码解析(二)Dubbo扩展机制SPI》。

(十)Transporters

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
复制代码public class Transporters {

static {
// check duplicate jar package
// 检查重复的 jar 包
Version.checkDuplicate(Transporters.class);
Version.checkDuplicate(RemotingException.class);
}

private Transporters() {
}

public static Server bind(String url, ChannelHandler... handler) throws RemotingException {
return bind(URL.valueOf(url), handler);
}

public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handlers == null || handlers.length == 0) {
throw new IllegalArgumentException("handlers == null");
}
ChannelHandler handler;
// 创建handler
if (handlers.length == 1) {
handler = handlers[0];
} else {
handler = new ChannelHandlerDispatcher(handlers);
}
// 调用Transporter的实现类对象的bind方法。
// 例如实现NettyTransporter,则调用NettyTransporter的connect,并且返回相应的server
return getTransporter().bind(url, handler);
}

public static Client connect(String url, ChannelHandler... handler) throws RemotingException {
return connect(URL.valueOf(url), handler);
}

public static Client connect(URL url, ChannelHandler... handlers) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
ChannelHandler handler;
if (handlers == null || handlers.length == 0) {
handler = new ChannelHandlerAdapter();
} else if (handlers.length == 1) {
handler = handlers[0];
} else {
handler = new ChannelHandlerDispatcher(handlers);
}
// 调用Transporter的实现类对象的connect方法。
// 例如实现NettyTransporter,则调用NettyTransporter的connect,并且返回相应的client
return getTransporter().connect(url, handler);
}

public static Transporter getTransporter() {
return ExtensionLoader.getExtensionLoader(Transporter.class).getAdaptiveExtension();
}

}
  1. 该类用到了设计模式的外观模式,通过该类的包装,我们就不会看到内部具体的实现细节,这样降低了程序的复杂度,也提高了程序的可维护性。比如这个类,包装了调用各种实现Transporter接口的方法,通过getTransporter来获得Transporter的实现对象,具体实现哪个实现类,取决于url中携带的配置信息,如果url中没有相应的配置,则默认选择@SPI中的默认值netty。
  2. bind和connect方法分别有两个重载方法,其中的操作只是把把字符串的url转化为URL对象。
  3. 静态代码块中检测了一下jar包是否有重复。

(十一)RemotingException && ExecutionException && TimeoutException

这三个类是远程通信的异常类:

  1. RemotingException继承了Exception类,是远程通信的基础异常。
  2. ExecutionException继承了RemotingException类,ExecutionException是远程通信的执行异常。
  3. TimeoutException继承了RemotingException类,TimeoutException是超时异常。

为了不影响篇幅,这三个类源码我就不介绍了,因为比较简单。

后记

该部分相关的源码解析地址:github.com/CrazyHZM/in…

该文章讲解了dubbo-remoting-api中的包结构设计以及最外层的的源码解析,其中关键的是理解端的概念,明白在哪一层才区分了发送和接收的职责,后续文章会按照我上面的编排去写。如果我在哪一部分写的不够到位或者写错了,欢迎给我提意见。

本文转载自: 掘金

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

1…848849850…956

开发者博客

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