东海龙宫这个项目开了
Prometheus(普罗米修斯)是一套开源的监控系统,其基本原理是通过 HTTP 协议周期性抓取被监控组件的状态,不需要任何 SDK 或者其他的集成过程,其架构如图:
Prometheus 主要由以下部分组成:
普罗米修斯的数据存储采用的是时间序列数据(TimeSeries Data),它是按照时间顺序记录系统、设备状态变化的数据。
默认情况下 Prometheus 会将采集的数据存储到本机的 /usr/local/prometheus/data
目录,存储数据的大小受限和扩展不便;如果只作为测试自然不需要担心这个问题,如果用于生产的话需要安装配置时序数据库influxdb
。
在 kubesphere 的安装中,普罗米修斯是配套安装的,前文介绍过kubesphere的安装教程。这里我直接使用现成的Prometheus系统。有安装需求的小伙伴建议使用k8s+helm进行安装。
KubeSphere 通过 NodePort 访问内置的 Prometheus 服务,服务类型更改为 NodePort,同时修改外部访问端口:
1 | kubectl edit svc -n kubesphere-monitoring-system prometheus-k8s |
访问普罗米修斯ip:port
我们可以通过这个操作页面进行一些指令操作,在指令栏输入KEY,它会有联想输入提前弹出你想要的KEY,然后点击执行按钮就能获得对应的监控数据:
在普罗米修斯监控中,称采集存储的数据为metrics,在普罗米修斯中它是以 key/value的形式保存的。其主要类型分为以下几种:
每个key-value 数据还会带上标签进行归类,标签可使用正则表达式进行匹配。
关于普罗米修斯采集到的 key-value 数据 可以访问 http://ip:port/metrics url 进行查看:
以 #
号开头的部分是对采集数值的一个说明,如:
1 | # HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime. |
HELP 是对这个采集数据的注释,TYPE 表示它的metrics类型为 counter。统计数据是 exporter 提供的,想要采集不同指标的数据 比如mysql 或者kafka 就要使用不同 expoerter 去收集,官方提供了不少exporter:
对于普罗米修斯的数据,我们不仅限于查看,还能进行一些函数运算:
1 | ## 查询最近2min |
这些语句称为pql,PQL使用”#”对语法进行注释,其常用内置函数有:
当然我们观察机器的一些数据指标肯定不能通过手写PQL去查看,这样就太累人了。通常我们会结合grafana进行可视化的监控。
grafana 是数据统计和展示工具,它展示数据,但不提供数据。目前Grafana 支持的数据源有:Graphite, InfluxDB, OpenTSDB, Prometheus, Elasticsearch, CloudWatch,Zabbix等。
grafana 相关概念:
grafana 部署我这里是采用 KubeSphere 的应用模板进行部署的,傻瓜式的安装,这里就不做太多介绍了,安装完成后界面如下:
然后导入prometheus数据源,Configuration → Data Sources → Prometheus → Select:
查看仪表盘:
当然grafana,还支持自定义仪表盘和查询统计语句,这种高定制化的需求需要对pql和 grafana 都有较深的理解
KubeSphere是k8s控制台,ubeSphere 目前提供了工作负载管理、微服务治理、DevOps 工程、Source to Image、多租户管理、多维度监控、日志查询与收集、告警通知、服务与网络、应用管理、基础设施管理、镜像管理、应用配置密钥管理等功能模块。
kubeSphere 帮我们把诸多云原生功能集中在一起并提供了web界面。利用KubeSphere我们可以根据我们之前学习的 Jenkins docker k8s 搭建一套完整的私有云系统,极大的减少运维以及开发的工作量。具体的搭建思路我在下一节中给出,这一节我们先安装并使用KubeSphere。
为了简化安装,我们这里使用的是KubeKey,KubeKey安装k8s的最低配置要求是2核4G,低于这个配置使用KubeKey会安装失败。由于KubeKey会访问github。所以需要保证你的主机能联网。我们本地实验的方式可以使用前文提到过的vagrant搭建虚拟机集群。然后在vagrant中安装。也可以在云上上实验,云上实验采用按量计费的方式,阿里云收费如下,网络带宽另计费
腾讯云的:
腾讯云的服务器要便宜很多,而且阿里云要使用按量计费需要余额大于100 元,腾讯云没有这个限制,不过两家都支持余额提现。做云实验的话我建议使用腾讯云的按量计费服务器,100块钱能玩很久(它的对象存储也只要几毛钱一个月)。
阿里云和腾讯云都推出了轻量级云服务器,比普通云服务器便宜很多,这种服务器是你用多少给你分配多少,比如我买了2核4g的服务器,如果我只用了1核1g,那剩余的资源就会被系统分配出去。这种服务器做实验体验不是很好,所以没考虑,感兴趣的小伙伴可以试试这种服务器。
为了偷个懒,这次我们实验就是在腾讯云上进行(主要是本地机器网络不太好)。
执行命令:
1 | export KKZONE=cn |
为 kk 添加可执行权限,并初始化本地主机:
1 | chmod +x kk |
接下来我们生成一个配置文件来安装k8s和kubeSphere
1 | ./kk create config [--with-kubernetes version] [--with-kubesphere version] [(-f | --file) path] |
示例:
1 | ## 使用默认配置创建示例配置文件 |
这里建议指定版本号,因为有的机器会不支持安装高版本kubeSphere,指定版本生成配置文件会有对应提示。
config-sample.yaml示例:
1 | apiVersion: kubekey.kubesphere.io/v1alpha2 |
安装前配置:
1 | ## 指定服务器hostname |
执行命令创建集群:
1 | ./kk create cluster -f config-sample.yaml |
安装过程比较耗时,中途可能出现安装失败的情况,可以使用该命令卸载再进行重装:
1 | ./kk delete cluster |
安装成功后会有如下日志:
安装完成后执行指令:
1 | kubectl get pod -A |
根据日志访问网页:
kk add nodes -f config-sample.yaml
kk delete node
kk delete cluster
kk delete cluster [-f config-sample.yaml]
kk upgrade [–with-kubernetes version] [–with-kubesphere version]
kk upgrade [–with-kubernetes version] [–with-kubesphere version] [(-f | –file) path]
1)Kubernetes 资源管理
支持工作负载管理、镜像管理、服务与应用路由管理 (服务发现)、密钥配置管理等
2)微服务治理
3)DevOps
基于 Jenkins 的可视化 CI / CD 流水线,支持从仓库 (GitHub / SVN / Git)、代码编译、镜像制作、镜像安全、推送仓库、版本发布、到定时构建的端到端流水线设置
4)监控
5)应用管理与编排
使用开源的OpenPitrix提供应用商店和应用仓库服务,提供应用全生命周期管理功能
k8s系列在这一篇算是终结了,下一篇会写普罗米修斯相关的文章,然后之后按照计划就是写我的 poseidon 项目了,目前对自己的要求就是一周一更新。
redis 的客户端有jedis、lettuce、redission;我个人比较推荐的是redission,因为它的分布式锁和缓存实在是太优秀了。 Redisson采用了基于NIO的Netty框架,封装了大家常用的集合类以及原子类、锁等工具。
本章节主要介绍redission 中重要的两个点:数据结构和锁
本章节主要介绍redission 中重要的两个点:数据结构和锁
基于Redis的Redisson的分布式映射结构的RMap Java对象实现了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。与HashMap不同的是,RMap保持了元素的插入顺序。
在特定的场景下,映射缓存(Map)上的高度频繁的读取操作,使网络通信都被视为瓶颈时,可以使用Redisson提供的带有本地缓存功能的映射。
代码示例:
1 | RMap<String, SomeObject> map = redisson.getMap("anyMap"); |
map本身也是可以上锁的:
1 | RMap<MyKey, MyValue> map = redisson.getMap("anyMap"); |
map中还有元素淘汰,本地缓存和数据分片等机制相关类:
元素淘汰(Eviction) 类 – 带有元素淘汰(Eviction)机制的映射类允许针对一个映射中每个元素单独设定 有效时间 和 最长闲置时间 。
本地缓存(LocalCache) 类 – 本地缓存(Local Cache)也叫就近缓存(Near Cache)。这类映射的使用主要用于在特定的场景下,映射缓存(MapCache)上的高度频繁的读取操作,使网络通信都被视为瓶颈的情况。Redisson与Redis通信的同时,还将部分数据保存在本地内存里。这样的设计的好处是它能将读取速度提高最多 45倍 。 所有同名的本地缓存共用一个订阅发布话题,所有更新和过期消息都将通过该话题共享。
数据分片(Sharding) 类 – 数据分片(Sharding)类仅适用于Redis集群环境下,因此带有数据分片(Sharding)功能的映射也叫集群分布式映射。它利用分库的原理,将单一一个映射结构切分为若干个小的映射,并均匀的分布在集群中的各个槽里。这样的设计能使一个单一映射结构突破Redis自身的容量限制,让其容量随集群的扩大而增长。在扩容的同时,还能够使读写性能和元素淘汰处理能力随之成线性增长。
map 类:
Redisson的分布式的RMapCache Java对象在基于RMap的前提下实现了针对单个元素的淘汰机制,这种功能是其他两个redis客户端所不能具备的。
Redis自身并不支持散列(Hash)当中的元素淘汰,因此所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。为了保证资源的有效利用,每次运行最多清理300个过期元素。任务的启动时间将根据上次实际清理数量自动调整,间隔时间趋于1秒到1小时之间。比如该次清理时删除了300条元素,那么下次执行清理的时间将在1秒以后(最小间隔时间)。一旦该次清理数量少于上次清理数量,时间间隔将增加1.5倍。
1 | RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap"); |
本地缓存功能充分的利用了JVM的自身内存空间,对部分常用的元素实行就地缓存,这样的设计让读取操作的性能较分布式映射相比提高最多 45倍 。以下配置参数可以用来创建这个实例:
1 | LocalCachedMapOptions options = LocalCachedMapOptions.defaults() |
RClusteredMap 分片map使用示例:
1 | RClusteredMap<String, SomeObject> map = redisson.getClusteredMap("anyMap"); |
映射监听器(Map Listener)可以监听map的活动,代码示例:
1 | RMapCache<String, Integer> map = redisson.getMapCache("myMap"); |
代码示例:
1 | RSet<SomeObject> set = redisson.getSet("anySet"); |
基于Redis的Redisson的分布式RSetCache Java对象在基于RSet的前提下实现了针对单个元素的淘汰机制。和map 一样,所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。代码示例:
1 | RSetCache<SomeObject> set = redisson.getSetCache("anySet"); |
分布式RClusteredSet:
1 | RClusteredSet<SomeObject> set = redisson.getClusteredSet("anySet"); |
有序集(SortedSet):
1 | RSortedSet<Integer> set = redisson.getSortedSet("anySet"); |
计分排序集(ScoredSortedSet)是一个可以按插入时指定的元素评分排序的集合:
1 | RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("simple"); |
RList 示例:
1 | RList<SomeObject> list = redisson.getList("anyList"); |
无界队列Queue:
1 | RQueue<SomeObject> queue = redisson.getQueue("anyQueue"); |
双端队列(Deque):
1 | RDeque<SomeObject> queue = redisson.getDeque("anyDeque"); |
阻塞队列(Blocking Queue):
1 | RBlockingQueue<SomeObject> queue = redisson.getBlockingQueue("anyQueue"); |
有界阻塞队列(Bounded Blocking Queue):
1 | RBoundedBlockingQueue<SomeObject> queue = redisson.getBoundedBlockingQueue("anyQueue"); |
阻塞双端队列(Blocking Deque):
1 | RBlockingDeque<Integer> deque = redisson.getBlockingDeque("anyDeque"); |
阻塞公平队列(Blocking Fair Queue):
1 | RBlockingFairQueue queue = redisson.getBlockingFairQueue("myQueue"); |
阻塞公平双端队列(Blocking Fair Deque):
1 | RBlockingFairDeque deque = redisson.getBlockingFairDeque("myDeque"); |
延迟队列(Delayed Queue):
1
2
3
4
5
6
7
8
9 RQueue<String> distinationQueue = ...
RDelayedQueue<String> delayedQueue = getDelayedQueue(distinationQueue);
// 10秒钟以后将消息发送到指定队列
delayedQueue.offer("msg1", 10, TimeUnit.SECONDS);
// 一分钟以后将消息发送到指定队列
delayedQueue.offer("msg2", 1, TimeUnit.MINUTES);
// 在该对象不再需要的情况下,应该主动销毁。仅在相关的Redisson对象也需要关闭的时候可以不用主动销毁。
delayedQueue.destroy();
优先队列(Priority Queue),可以通过比较器(Comparator)接口来对元素排序:
1 | RPriorityQueue<Integer> queue = redisson.getPriorityQueue("anyQueue"); |
如果负责储存分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
1 | RLock lock = redisson.getLock("anyLock"); |
Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开:
1 | // 加锁以后10秒钟自动解锁 |
异步执行:
1
2
3
4
5 RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程,所有请求线程会在一个队列中排队。当某个线程出现宕机,Redisson会等待5秒后继续下一个线程:
1
2
3 RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例:
1 | RLock lock1 = redissonInstance1.getLock("lock1"); |
红锁RedissonRedLock对象可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例:
1 | RLock lock1 = redissonInstance1.getLock("lock1"); |
同JDK中的信号量:
1 | RSemaphore semaphore = redisson.getSemaphore("semaphore"); |
可过期信号量,是在RSemaphore对象的基础上,为每个信号增加了一个过期时间:
1 | RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore"); |
同jdk中的闭锁:
1 | RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); |
redis持久化方式有两种,一种是RDB,一种是aof。
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。
AOF持久化以日志的形式记录服务器所处理的每一个写操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
有两个Redis命令可以用于生成RDB文件–SAVE和BGSAVE。SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
和SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令fork一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求。
创建RDB文件的实际工作由rdb.c/rdbSave函数完成,SAVE命令和BGSAVE命令会以不同的方式调用这个函数,源码:
1 | /* Save the DB on disk. Return C_ERR on error, C_OK on success. */ |
它的大致流程如下:
RDB文件保存的是二进制数据,文件结构如图所示:
RDB文件的最开头是REDIS部分,保存着“REDIS”五个字符,程序在载入RDB文件时会校验这个头数据以判断是不是RDB文件。
db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号。
databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据。
EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当程序读取到这个值的时候就表示所有数据已经读取完毕了。
check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。
服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,类似于mysql 的binlog。
AOF持久化保存数据库状态的方法是将服务器执行的SET、SADD、RPUSH等命令保存到AOF文件中,其过程大致如下:
AOF也有不同的触发方案,这里简要描述以下三种触发方案:
我们看一下flushAppendOnlyFile的注释:
大致意思是解释了刷盘时机以及force参数的含义及作用。
AOF日志文件会随着redis运行变得越来越大,如果服务器宕机重启,那么载入AOF文件就会很耗时。在AOF文件中有很多记录是可以被优化掉的,比如我现在将一个数据 incr 一千次,那么就不需要去记录这1000次修改,只需要记录最后的值即可,所以就需要进行 AOF 重写。Redis 提供了bgrewriteaof指令用于对AOF日志进行重写,该指令会拉起一个子进程对内存进行进行遍历并转换为一系列redis指令,最后保存到一个日志文件中并替换掉原有AOF文件。
随着Redis的运行,AOF的日志会越来越长,如果实例宕机重启,那么重放整个AOF将会变得十分耗时,而在日志记录中,又有很多无意义的记录,比如我现在将一个数据 incr 一千次,那么就不需要去记录这1000次修改,只需要记录最后的值即可。所以就需要进行 AOF 重写。同样的也可以在redis.config中对重写机制的触发进行配置:
1 | ### 开启重写机制 |
AOF 优势:
AOF 劣势:
RDB 优势:
RDB劣势:
在进行容灾恢复时,如果使用 RDB 来恢复内存状态,可能会会丢失大量数据。
而如果只使用 AOF 效率又很低。Redis 4.0 提供了混合持久化方案,将 RDB 文件的内容和增量的 AOF日志文件存在一起。
这里的 AOF 日志不再是全量的日志,而是自 RDB 持久化开始到持久化结束这段时间发生的增量 AOF 日志,通常这部分日志很小。
当进行容灾恢复或redis重启的时候,就可以先加载RDB数据,然后AOF日志补全RDB数据以达到高性能可靠的备份恢复。
在redis源码中数据库的结构由server.h/redisDb表示,
redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space),redisDb源码:
1 | typedef struct redisDb { |
源码中redisDb拥有字典属性dict
,字典中存储了数据库中的键,为字符串类型的redisObject。这个redisObject中的ptr属性指向值的redisObject,结构示意图:
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作:
通过EXPIRE命令或者PEXPIRE命令可以设置键的过期时间,那么在数据库中这个过期时间是怎么维护的呢?redisDb结构体中有一个字典属性expires
便是用来保存键的过期时间的,
我们称这个字典为过期字典。过期字典的键是一个指针,指向键空间中的键;过期字典的值是一个long类型的数,记录了过期时间的时间戳。当客户端执行PEXPIREAT命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间。
如果现在给key设置一个过期时间,在过期时间到的时候,Redis是如何清除这个key的呢?Redis 中提供了三种过期删除的策略:
Redis 中实际采用的策略是惰性删除加定期删除的组合方式,服务器会定期清除掉一部分过期的key,对于那些未清除到的过期key,会在获取这个key的时候进行判断是否过期,过期则删除。
惰性删除会带来一个问题就是当从从库获取一个过期key的时候从库是否应该删除这个key呢?如果一个主库创建的过期键值对,已经过期了,主库在进行定期删除的时候,没有及时的删除掉,这时候从库请求了这个键值对,当执行惰性删除的时候,因为是主库创建的键值对,这时候是不能在从库中删除的。从库会通过惰性删除来判断键值对的是否过期,如果过期则读不到这个键,真正的删除是当主节点触发键过期时,主节点会同步一个del命令给所有的从节点。
我们知道redis 持久化策略中包括RDB持久化功能、AOF持久化,这两种持久化对过期未删除的键处理也是有区别的。RDB持久话不会保存过期未删除的键,而AOF持久化当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显式地记录该键已被删除。
在 Redis 命令中,有一些命令是阻塞模式的,BRPOP, BLPOP, BRPOPLPUSH, 这些命令都有可能造成客户端的阻塞。比如向客户端发来一个blpop key命令,redis先找到对应的key的list,如果list不为空则pop一个数据返回给客户端;如果对应的list不存在或者里面没有数据,就将该key添加到redisDb 的blockling_keys的字典中,value就是想订阅该key的client链表。并将对应的客户端标记为阻塞。
如果客户端发来一个repush key value命令,先从redisDb的blocking_keys中查找是否存在对应的key,如果存在就往redisDb的ready_keys这个链表中添加该key;同时将value插入到对应的list中,并响应客户端。redis处理完客户端命令后都会遍历ready_keys和blockling_keys来筛选出需要pop出的clinet。因此,redis客户端的阻塞是通过ready_keys和blockling_keys联合来实现的,blockling_keys 记录阻塞中的key和客户端,ready_keys记录数据已准备好的key。
前文我们看过redisObject
的源码:
1 | typedef struct redisObject { |
下面我们来了解redisObject
相关机制
我们知道redis的键和值都是以redisObject的形式保存的,而键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。我们执行TYPE
指令可以查看键对应的值的属性:
1 | redis>TYPE test |
这个指令就是查看redisObject中的type属性类型,前文我们也提到过对象的类型包括string,list,hash,set,zset。
我们知道del、expire、remane、type等命令是可以使用在各种类型的redisObject上的,而类似lpush、llen等指令就只能用在对应的type上,比如对string类型的redisObject使用llen 结果将会这样:
1 | redis>llen test |
为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。
在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象的type属性是否为执行命令所需的类型,如果是的话服务器就对键执行指定的命令,否则就抛出警告。
为了实现类似jvm的内存回收机制,Redis在自己的对象中添加了一个引用计数属性–refcount,通过这个值程序可以在适当的时候自动释放对象并进行内存回收。
对象的引用计数值随着redisObject生命周期的变化:
对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。
除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。
假如 A键存储一个”1000”的整数值字符串对象,同时B键也存储了一个”1000”的整数值字符串对象,此时reids只会创建一个”1000”的整数值字符串对象,而它的引用计数会增一。
也就是说到多个key之间可以共享一个对象的时候,只会创建一个对象,而引用计数会相应的增一,另外这种优化只针对整数值字符串对象。目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。object refcount
指令是可以查看对象的引用数的,下面我们来做一个实验验证:
1 | local:0>set a1 100 |
第一次set a1 时查看引用计数是2,引用这个值对象的两个程序分别是持有这个值对象的服务器程序,以及共享这个值对象的键a1。
第二次set a2 同样的值发现对象的引用计数变成了3,和我们的理论是一致的。然后我们验证一下字符串:
1 | local:0>set b1 xxxx |
我们发现字符串类型之间不存在对象共享,因为字符串的对象共享的验证计算成本比较高,redis出于性能考虑不对字符串类型的对象进行共享。
redisObject的lru属性,该属性记录了对象最后一次被命令程序访问的时间。查看这个属性的指令为object idletime
,这个命令在访问键的值对象时,不会修改值对象的lru属性:
1 | local:0>object idletime b1 |
如果redis服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。db.c 中有一个 lookupKey 方法:
1 | /* Low level key lookup API, not actually called directly from commands |
每次按key获取一个值的时候,都会调用lookupKey函数,如果配置使用了lru模式,该函数会更新value中的lru字段为当前秒级别的时间戳。虽然记录了redisObject的时间戳,但淘汰键时肯定不能遍历比较这个lru值,
这样做计算量太大。实际上redis是这样干的:
实际上redis 淘汰策略还有一个lfu算法,该算法采用的是通过记录key的访问次数来选择需要淘汰的key,感兴趣的小伙伴可以百度相关资料。
redis中没有直接使用C语言的字符串,而是自定义了一种名为简单动态字符串的抽象类型——SDS。我们下载redis源码,可以在src目录下找到一个sds.h
的文件,打开这个文件查看它的部分代码:
1 |
|
根据代码注释我们知道:
因此sds示意图就是这样的:
那么redis为什么要这么设计呢,出于以下几点考虑:
在redis 源码中链表的定义可以通过adlist.h
查看:
1 | /* Node, List, and Iterator are the only data structures used currently. */ |
从源码我们可以看出链表由三个结构体来维护,list
\ listNode
\ listIter
。list结构为链表提供了表头指针 head,表尾指针 tail,链表长度 len。redis 链表有以下特点:
字典即map,redis字典使用哈希表作为底层的实现,每个哈希表节点中保存字典中的一个键值对。在redis的源码中可以通过dict.h
查看字典的定义:
1 | typedef struct dictEntry { |
我们看到源码中有dictType
,dictEntry
,dict
,dictIterator
,dictht
这几个结构体来维护字典结构,(7.0以后版本无dictht)。
其中dictIterator
为字典的迭代器,dictEntry
结构保存着一个键值对,dictEntry
属性说明:
结构体dictType
定义了一堆用于处理键值的函数,我们可以不去关心。
dictht
是一个哈希表结构,它通过将哈希值相同的元素放到一个链表中来解决冲突问题,属性说明:
结构体dict
包含的属性有:
dictType
结构的指针;dictht
哈希表;redis的hash算法使用的是MurmurHash2
,具体算法细节不做介绍。随着对hash的操作其中的键值对会发生改变,这个时候为了更合理的分配空间就需要进行hash重算(rehash)。
在dict中ht属性是一个长度为2的dictht
数组,当进行hash重算的时候会将ht[0]的键值对rehash到ht[1]里面。rehash这个过程不是一次性完成的,是多次渐进式地去完成的。
rehash过程:
这种方式主要是为了避免集中的rehash所带来的庞大计算量。因为渐进式rehash会同时使用ht[0]和ht[1],所以在rehash期间redis对这个字典的更新查找等操作会同时在这两个ht中进行。
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指针指向其他节点从而实现跳跃访问其他节点,zset的底层便是跳跃表。在redis源码中server.h
定义了跳跃表的结构:
1 | /* ZSETs use a specialized version of Skiplists */ |
我们看到zset 是由 dict
(集合)和zskiplist
(跳表)组成,zskiplist又包含了如下属性:
其中 header 和 tail 是结构体zskiplistNode
的指针,这个结构体便是跳表的节点,它有如下属性:
跳表的层可以包含多个元素,每个元素都包含指向一个节点的指针用于快速访问其他节点,比如程序访问节点1,节点的层包含了节点4的层,那么就可以跳跃到节点四,而不是一直遍历到节点4。
redis 使用对象来表示数据库中的键值,当我们在redis数据库中创建一个键值对时,至少会生成两个对象,用于表示key和value。redis对象源码:
1 | typedef struct redisObject { |
属性说明:
redis 命令的多态,内存回收,内存共享,内存淘汰策略等特性都涉及到 redisObject,下一章节将单独讲解相关特性,感谢阅读。
vagrant 相对于 vmware 而言更轻量级,操作更简便移植性更强,如果我们需要学习k8s或者搭建一些集群的话建议使用 Virtualbox+Vagrant。
Vagrant 是创建虚拟机的工具,Virtualbox 是vagrant 管理工具,而且这两个软件是开源的,不需要我去付费或者破解。
vagrant 下载地址:https://www.vagrantup.com
virtualbox 下载地址:https://www.virtualbox.org/wiki/Downloads
下载安装完成后,我们还需要下载vagrant镜像
镜像下载地址:https://app.vagrantup.com/boxes/search
打开cmd 窗口,添加本地镜像:
1 | vagrant box add --name 镜像名称 E:data/centos7.box |
执行 vagarnt init
指令会根据镜像在当前文件夹生成一个Vagrantfile
文件,这个文件是创建虚拟机的配置文件。
打开 virtualbox,管理->常规->默认虚拟电脑位置,设置虚拟机存储文件夹。
打开cmd 输入 ipconfig
查看VirtualBox 的虚拟网卡ip:
1 | 以太网适配器 VirtualBox Host-Only Network: |
修改 Vagrantfile
文件,指定其虚拟机的内网ip,需要和虚拟网卡一个网段:
1 | ### 配置位置可文件中搜索该配置项,有相应的提示 |
在 Vagrantfile
所在文件夹打开powershell或者cmd 执行指令vagrant up
启动一个虚拟机。
待虚拟机启动完成后执行 vagrant ssh
进入虚拟机,该虚拟机的root默认密码为vagrant
,进入时的账号也是vagrant
:
1 |
|
我们在搭建一些环境时往往需要启动多台虚拟机,接下来我们将介绍如何创建多台虚拟机并进行相关的配置。
我们修改Vagrantfile
内容:
1 | Vagrant.configure("2") do |config| |
执行 vagrant up
启动node1,node2,node3 三台虚拟机,在启动过程中可能会报挂载失败的错误:
1 | node1: /share => E:/vagrant/data |
安装插件可以解决(如果还报错则卸载插件,换一个更低版本的):
1 | ## 安装插件 |
执行 vagrant status
查看虚拟机运行状态,也可以直接在virtualbox 界面上查看,在下次启动虚拟机的时候就不需要再cmd窗口执行vagrant up指令
,直接在virtualbox界面上选择启动方式。
进入虚拟机的指令:
1 | vagrant ssh '虚拟机名称' |
在我们虚拟机启动后,其中dns 服务器地址是有问题的,我们希望在创建虚拟机的时候,进行一些基础的配置,我们可以在Vagrantfile 中添加脚本实现这些配置:
1 | Vagrant.configure("2") do |config| |
在示例中,我通过配置项node.vm.provision "shell"
编写脚本设置了dns 服务器ip。通过这个方式我们可以实现预装docker等操作。
我们还可以指定外部脚本,配置示例:
1 | config.vm.provision "shell", path: "script.sh" |
当然我们还可以制作自己的box 实现一键创建虚拟机。
首先我们在原来的虚拟机中安装好软件并修改相关配置配置文件,然后清除掉private_network的网络规则:
1 | sudo rm -f /etc/udev/rule.d/70-persistent-net.rules |
检查 /etc/ssh/sshd_config文件PasswordAuthentication no 是否被注释掉,没有注释掉的话无法通过 vagrant ssh
登录。
然后执行指令:
1 | vagrant package node1 |
在当前文件夹下得到一个 package.box 。
vagrant package 极大的增强了虚拟机的可移植性,在一定程度上降低了我们的学习成本。
vagrant 常用指令速查表:
1 | # 查看box |
1 | nmcli connection show |
kubernetes集群由master、node构成。其中master包含组件:
node 节点包含组件:
搭建k8s开发环境有三种,一种是通过docker desktop + Minikube 来直接在你的电脑上搭建,这种搭建方式存在的问题比较多,很多功能不支持,不建议使用。
另外一种方式是通过Docker Desktop安装k8s,这种k8s是单机版的,master 和node 是同一个节点也就是本机,这种方式安装的k8s基本上能满足我们的学习需求,初期学习阶段可以使用这种安装方式。
还有就是通过前文介绍 vagrant 制作box 然后创建集群安装,这种安装方式是最完整也是最麻烦的。
现在先介绍第二种安装方式,第二种方式是把k8s 镜像拉取下来并运行容器,但因为国内网络的问题,镜像依赖拉不下来,我们可以上github 拉阿里云的k8s-for-docker-desktop 到本地安装。
1 | git clone https://github.com/AliyunContainerService/k8s-for-docker-desktop.git |
需要注意git tag 是不是和你的 docker中的k8s版本保持一致
然后打开你的docker desktop,勾选k8s:
等docker 重启后就安装完成了,打开命令行窗口执行指令,验证是否安装成功:
1 | kubectl cluster-info |
第三种安装方式需要kubeadm 来进行集群安装,k8s集群可以一主多从或者多主多从,这里我搭建的是一主多从集群。
安装步骤:
制作自己的box并配置好Vagrantfile,box中需要安装docker,更新yum源等,最好安装一些基本工具与telnet,然后启动虚拟机
禁用selinux,禁用swap分区 ,selinux是linux系统下的一个安全服务,如果不关闭它,在安装集群中会产生各种各样的奇葩问题,swap分区指的是虚拟内存分区,
它的作用是物理内存使用完,之后将磁盘空间虚拟成内存来使用,启用swap设备会对系统的性能产生非常负面的影响
修改linux的内核参数
1 | # 修改linux的内核采纳数,添加网桥过滤和地址转发功能 |
1 | # 1、由于kubernetes的镜像在国外,速度比较慢,这里切换成国内的镜像源 |
1 | # 在安装kubernetes集群之前,必须要提前准备好集群需要的镜像,所需镜像可以通过下面命令查看 |
1 | kubeadm join 192.168.18.1:8080 --token xxx \ |
1 | kubectl get nodes |
命名空间在k8s中的主要作用是资源隔离,可以将多个 pod 放入同一namespace中,实现对一组pod资源的管理。
k8s 在集群启动后,会默认创建几个namespace ,可以通过指令kubectl get ns
查看:
1 | C:\Users\Administrator>kubectl get ns |
如果我们创建pod的时候未指定namespace,则会默认划分到 default 命名空间中,kube-node-lease
用于维护节点之间的心跳,kube-public
公共资源的命名空间,可以被所有人访问,kube-system
k8s的系统资源命名空间。
查看命名空间指令:
1 | kubectl get pods -n kube-system |
namespace 相关指令:
1 | ### 查看 |
在namespace 属性中 status 的状态包括 active Terminating(正在删除),resource quota 属性限制了 ns的资源,limitRange resource 限制了 ns 下 pod的资源
我们还可以以配置文件的形式创建namespace,创建ns-dev.yaml:
1 | apiVersion: v1 |
执行对应的创建或删除命令:
1 | kubectl create -f ns-dev.yaml |
pod 是k8s集群进行部署管理的最小单元,一个pod中可以有一个或者多个容器,pod是对容器的封装。k8s 在集群启动后,集群中的各个组件都是以pod的方式运行的。可以通过以下命令查看:
1 | kubectl get pod -n kube-system [-o wide] |
pod相关指令:
1 | ### 运行一个pod |
通过配置文件创建pod:
1 | apiVersion: v1 |
然后就可以执行对应的创建和删除命令了:
创建:kubectl create -f pod-nginx.yaml
删除:kubectl delete -f pod-nginx.yaml
Label是kubernetes系统中的一个重要概念。它的作用就是在资源上添加标识,用来对它们进行区分和选择。
Label的特点:
可以通过Label实现资源的多维度分组,以便灵活、方便地进行资源分配、调度、配置、部署等管理工作。
一些常用的Label 示例如下:
- 版本标签:”version”:”release”, “version”:”stable”……
- 环境标签:”environment”:”dev”,”environment”:”test”,”environment”:”pro”
- 架构标签:”tier”:”frontend”,”tier”:”backend”
打标签命令
1 | kubectl get pod -n dev --show-labels |
配置文件打标签
1 | apiVersion: v1 |
在pod的yaml 中指定标签然后通过指令:
1 | kubectl apply -f nginx.yaml |
完成更新
在kubernetes中,Pod是最小的控制单元,但是kubernetes很少直接控制Pod,一般都是通过Pod控制器来完成的。Pod控制器用于pod的管理,确保pod资源符合预期的状态,当pod的资源出现故障时,会尝试进行重启或重建pod。
在kubernetes中Pod控制器的种类有很多,本章节只介绍一种:Deployment。
1 | ## 查看deployment pod |
配置文件创建:
1 | apiVersion: apps/v1 |
通过上节课的学习,已经能够利用Deployment来创建一组Pod来提供具有高可用性的服务。
虽然每个Pod都会分配一个单独的Pod IP,然而却存在如下两问题:
这样对于访问这个服务带来了难度。因此,kubernetes设计了Service来解决这个问题。
Service可以看作是一组同类Pod对外的访问接口。借助Service,应用可以方便地实现服务发现和负载均衡。
1 | # 暴露Service |
docker是Docker.inc 公司开源的一个基于LXC技术之上构建Container容器引擎技术,Docker基于容器技术的轻量级虚拟化解决方案,实现一次交付到处运行。
docker实现程序集装箱的概念,把我们需要交付的内容集装聚合成一个文件(镜像文件)直接交付。
docker 架构图:
从架构图中我们可以看出,docker有三大核心,包括容器,仓库,镜像
镜像(image):文件的层次结构,以及包含如何运行容器的元数据
容器(container):容器是镜像创建的运行实例,它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。可以把容器看作是一个简易版的linux环境,Docker利用容器来运行应用
仓库(repository):仓库是集中存放镜像文件的场所,仓库注册服务器上往往存放着多个仓库,每个仓库中又保存了很多镜像文件,每个镜像文件有着不同的标签。
docker 具有如下特性:
文件系统隔离:每个进程容器运行在完全独立的根文件系统中
资源限制:每个进程容器运行在自己的网络命名空间中,拥有自己的虚拟接口和ip地址等
写时复制:由于镜像采用层式文件系统,所以采用写时复制方式创建镜像的根文件系统,这让部署变得极其快捷,并且节省内存和硬盘空间
日志记录:docker会收集和记录每个进程容器的标准流,用于实时检索或批量检索。不消耗本地io
变更管理:容器文件系统的变更可以提交到新的镜像中,并可以重复使用以创建更多容器。
交互式shell:docker可以分配一个虚拟终端并关联到任何容器的标准输入上。
namespace隔离:每个进程容器运行在自己的网络命名空间里,拥有自己的虚拟接口和ip地址等
docker 工作流程图:
docker 工作流程大体分为三步:
docker容器及镜像结构:
Docker 支持通过扩展现有镜像,创建新的镜像,新镜像是从 base 镜像一层一层叠加生成的,每新增一个应用,就会叠加一层镜像。
镜像分层的好处就是共享资源,比如说有多个镜像都从相同的 base 镜像构建而来,那么 Docker 只需在磁盘上保存一份 base 镜像,
同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。
当容器启动时,一个新的可写层被加载到镜像的顶部,这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
所有对容器的添加、删除、还是修改文件都只会发生在容器层中。
只有容器层是可写的,容器层下面的所有镜像层都是只读的。
docker 原理我们基本普及了,接下来我们进入实战环节。
接下来我们将在windows操作系统上安装docker desktop,需要注意的地方就是windows系统不能是家庭版的,需要开启虚拟化,需要安装WSL2。
具体的流程我就不介绍了,网上能找到比较多的例子
docker 安装完成之后,我们可以运行一个hello world 镜像测试:
1 | docker run hello-world |
命令行窗口输出拉取镜像运行的日志,接下来对镜像和容器进行查看删除等操作:
1 | ## 查看正在运行的容器 |
接下来我们创建一个springboot应用并制作成镜像,maven依赖:
1 | <dependencies> |
然后在pom文件的同级目录下创建 Dockerfile:
1 | FROM openjdk:8-jdk-alpine |
dockerfile-maven-plugin
是制作镜像的maven插件,插件和build默认绑定,执行build阶段运行该插件,push绑定到deploy阶段。
而dockerfile是制作镜像的描述文件。
Dockerfile是一个文本文件,其内包含了一条条的指令(Instruction),用于构建镜像。每一条指令构建一层镜像,因此每一条指令的内容,就是描述该层镜像应当如何构建。
Dockerfile参数说明:
在我们执行 mvn package
指令时会在命令行输出整个docker镜像的制作过程,并在后续能在docker中通过docker images 查看该镜像。
制作好的镜像只是存在我们的本地中,我们可以推到远程仓库到其他机器上运行,而几大云平台都提供了免费的远程私有仓库,比如阿里云效和腾讯云coding。
后续如果有时间会出Jenkins+docker+springboot的详细教程介绍如何一键远程部署我们的应用。
ansible是一个基于python的自动化运维工具,实现了批量系统配置、批量程序部署、批量运行命令等功能。ansible包括部署和使用简单、默认使用ssh协议、轻量级等特点。
ansible 架构:
https://www.cnblogs.com/keerya/p/7987886.html
懒得写
jenkins 内置四种构建触发器:
此外还可以通过安装插件通过git hook 自动触发构建
我们可以通过访问jenkins 提供的链接触发jenkins流水线进行构建,如图所示:
配置好令牌后访问地址:
1 | http://localhost:9901/job/test2/build?token=test |
再控制台上就能看到一次构建记录
当其他流水线执行后,触发当前流水线执行,如图所示:
从图中我们能看到它的触发规则有四种
即Build periodically,它通过cron表达式定时执行我们的流水线,如图所示:
点击标题旁边的问号图标,Jenkins会给予相关的说明和示例,我们照着示例去配置即可,配置示例:
1 | # Every fifteen minutes (perhaps at :07, :22, :37, :52): |
定时去扫描流水中配置的代码仓库,检测是否有变更,如果代码有变更则触发流水线执行,我们需要配置轮询规则,配置方式和定时构建一样:
以github 为例,当github 发生代码提交的时候,github向jenkin 发送构建请求以执行流水线。
在github 上配置token并设置webhook:
登录github 访问链接https://github.com/settings/tokens,点击Generate new token,配置权限 repo,admin:repo_hook:
点击保存,获取 token,保存好这个token
在github对应的代码仓库中选择设置–>webhooks
在jenkins中安装github 插件,我们需要对插件进行一些配置以实现相关功能,配置界面如图所示:
填写 API URL为https://api.github.com
点击添加按钮,类型选择Secret Text
Secret 填token,其余随意。
然后在流水线的构建触发器中勾选GitHub hook trigger for GITScm polling 就ok拉:
pipeline是部署流水线,它支持脚本和声明式语法,能够比较高自由度的构建jenkins任务.个人推荐使用这种方式去构建jenkins。
Jenkins 1.x只能通过界面手动配置来配置描述过程,想要配置一些复杂度高的任务,只能选择自由风格的项目,通过选项等操作进行配置,让jenkins可以下载代码、编译构建、然后部署到远程服务器上,这样显然是不方便管理和移植的。
pipeline的功能由pipeline插件提供,我们可以创建一个jenkinsfile来申明一个任务。接下来我们创建一个最简单的pipeline。登录jenkins,点击创建item:
在流水线中选择hello world 生成代码:
以上便是一个最简单的流水线。点击build now,jenkins任务开始执行,运行完成后点击查看执行记录:
在console output 中可以看到运行记录:
为了提高流水线的复用性以及便于流水线代码的管理,更多的是将pipeline的脚本在远程仓库,当我们修改了远程仓库的流水线脚本,jenkins就会加载到最新的脚本执行。
在流水线配置中选择pipeline script from SCM:
按照提示配置好脚本仓库地址,访问仓库的凭证,流水线脚本文件的名称(默认是Jenkinsfile),分支(默认是master)等。配置完成后在仓库中添加文件Jenkinsfile
把脚本粘贴过去并push,
最后执行任务,发现执行成功。通过这个特性,我们可以把我们的流水线脚本和项目代码本身放到一个仓库中管理,达到多版本控制并和代码版本统一的效果。
如果我们编写jenkinsfile需要语法提示相关的编辑器,可以使用jenkins官方提供的vscode插件Jenkins Pipeline Linter Connector
。使用idea Groovy 也能提示部分语法。
idea 设置jenkinsfile 语法提示方法 settings > editor > File Types > Groovy 新增一列Jenkinsfile:
jenkins pipeline有2种语法:脚本式(Scripted)语法和声明式(Declar-ative)语法。pipeline插件从2.5版本开始同时支持两种语法,官方推荐的是使用申明式语法,在这里也只对申明式语法进行介绍。
申明式语法demo:
1 | pipeline { |
声明式语法中,以下结构是必须的,缺少就会报错:
接下来我们编译一个本地项目,流水线脚本示例:
1 | pipeline { |
前文提到过,jenkins会给每个任务在workspacedir下创建文件夹作为运行环境,接下来我们验证通过git将代码下载到这个文件夹下然后打包。流水线脚本:
1 | pipeline { |
我配置的workspace 路径是’E:\Temp\jenkins\workspace’,任务名称是 test,看Jenkins 执行指令就能看到相关信息:
打开这个文件夹看看是什么情况:
步骤check out
把我github上的项目拉到这个工作目录下了,而Build
则是对项目进行了编译,然后我们可以在target目录找到编译好的jar包,在实际项目中我们可以通过指令将这个jar推到远程服务器上去,或者可以做成docker镜像,推到docker仓库,在远程执行docker指令把这个镜像跑起来,maven插件dockerfile-maven-plugin
是可以直接通过一个dockerfile 文件将项目打成一个镜像的。而jenkins 插件SSH Pipeline Steps
可以远程执行shell 脚本,这样整个流程就串通起来了。这个插件的github地址: https://github.com/jenkinsci/ssh-steps-plugin#pipeline-steps。
插件脚本示例:
1 | stage('部署镜像') { |
到这里,我们基本上已经掌握了jenkins的基本使用,可以完成将项目下载,编译部署等功能。接下来文章我们会学习一些jenkins的更复杂用法。
Jenkins是一款开源 CI&CD 软件,用于自动化各种任务,包括构建、测试和部署软件,CI&CD:
jenkins 有2种部署方式,war包直接启动和tomcat方式启动。推荐采用tomcat方式启动,方便进行日志查看和管理。启动jenkins并访问 `http://127.0.0.1:8080/jenkins
`,初始密码保持在initialAdminPassword 文件中,初始化过程会要求安装插件,选择推荐插件,若安装失败可在左上角/右上角找到跳过,进行跳过。因为Jenkins插件默认下载地址是国外,会很容易出错,后续可以更换为国内镜像仓库再进行插件安装。完成初始化过程后,进入界面修改插件代理,manage plugins——>高级页签:
URL填:https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
然后保存,在重试装插件,插件就能装成功了。接下来,我们配置凭据,git等通用配置。
manage jenkins -> global tool configuration :
如图所示,你可以选择让jenkins为你安装git,也可以配置机器上已安装好的git,jdk和maven同理。
manage jenkins -> manage Credentials 配置凭证:
这个凭据就是你的ssh私钥,我们拉取github或者gitlab上的代码的时候都会在自己账号上配置一个公钥,然后我们就能通过ssh拉取代码了,这个私钥就是用来拉取远程代码用的。
jenkins会有一个workspace文件夹,这个文件夹下会根据流水线创建对应的文件夹并在对应的文件夹下进行编译运行脚本。我们可以修改它的默认路径,重启jenkins生效,首先找到jenkins配置文件路径,manage jenkins->configure System:
然后在该文件夹下找到对应的配置文件 config.xml:
最后修改配置项workspaceDir并重启:
到此,Jenkins的基本安装配置已经完成,下一章节我们会介绍pipeline,并用pipe编译一个项目,感谢阅读
之前我做过一款 chrome代理插件——poseidon-chrome-proxy,这个插件的功能是通过一些配置将浏览器中的请求代理到你配置的服务器上去。这款插件的局限性是只能使用在谷歌浏览器中,而且无法代理https请求,这是因为谷歌浏览器限制了pac脚本对https请求的代理;不仅如此,该插件还存在dns污染的问题,虽然可以通过清除浏览器缓存来解决,但是也是比较糟心。最近项目中恰好遇到了需要对https进行代理的需求,经过我的研究,最终找到了一个比较满意的解决方案,它就是fiddler。
fiddler 是一款专门用于抓取http请求的抓包工具,当启动该工具时,pc端的请求会先被代理到该工具再转发到服务器,因此我们就可以在请求转发前对请求的协议,请求头,路径,请求内容等信息进行修改。而且通过该工具你还能记录某个请求的数据并进行回放或打断点,相比较代理插件,不仅适用范围更广,调试bug也更方便。
官方下载地址:https://www.telerik.com/download/fiddler/fiddler4,安装完成后我们进行一些配置:
点击 winConfig,勾选要代理的应用
点击 rules:
勾选 hide connects,隐藏连接信息,这些信息对我们来说是不需要去关注的。
点击 filters, 勾选 use filters ,配置我们需要调试的域名,我这里配置的是 www.baidu.com;该配置会过滤掉无关的域名,使其不会在左侧列表中显示。
fiddlerScript 是这个工具的重头戏,如图所示,该脚本中包含多个方法:
我们可以通过编辑这个脚本实现对请求和响应数据的修改,其语法使用的是javascript。编辑完脚本后需要点击编辑器左上角的 save script 按钮脚本才能生效。
FiddlerScript 中的主要方法包括:
一些修改请求数据的示例,
修改cookie:
1 | static function OnBeforeRequest(oSession: Session) |
修改 requestbody:
1 | static function OnBeforeRequest(oSession: Session) |
修改请求路径:
1 | static function OnBeforeRequest(oSession: Session) |
点击rules-> automatic breakpoint ->before requests 设置全局断点:
然后我们用浏览器发起一个请求,再观察fiddler:
此时该请求上会有一个 “T” 字形的标记,这个就是进入了断点,我们点击这个请求。在右侧界面可以观察并修改这个请求的数据,包括请求头,请求url,请求体等。修改完成后按上方的 “GO” 按钮执行。然后在右下方就会展示响应信息,如果我们打了 “after Responses” 则会进入断点。
1 | ## 在main()方法上方添加 |
1 | #if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end |
.link:
1 | after: |
.list:
1 | List<$EXPR$> $END$=new ArrayList<>(); |
.strmap
1 | Map<String,$EXPR$> $END$=new HashMap<>(); |
1 | /error: |
1 | \b(tag)\b.* |
maven->runner:
1 | -Dmaven.test.skip=true |
Thread count:
带C指定核数,不带C指定线程数
1 | <?xml version="1.0"?> |
1 | > 标准输出重定向 覆盖输出 |
1 | ## 从第3行开始显示,显示接下来10行内容: |
du -h --max-depth=1 /
df -h /
find / -name root -type d
find / -name test.log
管道一般用于过滤, A|b 命令A的正确输出作为命令B的操作对象
grep 取出含有搜寻内容的行 -v 反选,:
1 | ## tail 出有关键字的日志并输出后10行 |
1 | # 将错误输出 标准输出丢弃 |
at 一次性计划任务
systemctl status atd
at now +1minutes
cron 周期性计划任务
crond
crontable
使用crontable 创建任务后任务会记录到/var/sponl/cron里面去
执行日志保存到/var/log/cron中
1 | ## 这里,我们在每天早上 8 点整执行 find 命令;该命令会在 /home/s/coredump 目录下寻找 search 用户创建的普通 7 天前的文件,然后删除 |
scp [-P22 端口号] local_file remote_username@remote_ip:remote_file
1 | sudo scp -o xxx xxx.jar root@192.168.1.1:/home/test |
#!/bin/sh是指此脚本使用/bin/sh来解释执行,#!是特殊的表示符,其后面根的是此解释此脚本的shell的路径。
变量
1 | if [ 条件 ] |
1 | #!/bin/bash |
用于配置开机自启动或者挂掉重启
配置示例:
1 | [Unit] |
其中配置 Restart=always
和 RestartSec=5
可在进程挂了之后重启,systemctl daemon-reload
重新加载配置。
Type:定义启动时的进程行为。它有以下几种值。
sentinel 增加规则的方式 包括三种,数据源加载,代码加载,控制台加载;每一类流控规则我都会从这三个方面去说明如何使用。
流量控制是通过监控应用流量的qps或者并发线程数是否达到阈值来保护应用的一种手段,避免应用被瞬时的流量高峰冲垮,从而保障系统的高可用。
流量控制的方式:
流量控制的相关概念:
流控规则代码方式配置示例:
1 |
|
流控规则控制台配置示例:
流控规则数据源json示例:
1 | [{"clusterConfig":{"acquireRefuseStrategy":0,"clientOfflineTime":2000,"fallbackToLocalWhenFail":true,"resourceTimeout":2000,"resourceTimeoutStrategy":0,"sampleCount":10,"strategy":0,"thresholdType":0,"windowIntervalMs":1000},"clusterMode":false,"controlBehavior":0,"count":1.0,"grade":1,"limitApp":"default","maxQueueingTimeMs":500,"resource":"test","strategy":0,"warmUpPeriodSec":10}] |
熔断降级会在调用链路中当某个资源指数超出阈值时对这个资源的调用进行熔断,在熔断时间窗口内所有调用都快速失败调用降级方法,直到熔断恢复;
降级熔断和流控规则的区别是在超出阈值后的时间窗内所有的调用都会被降级,直到熔断恢复。
熔断降级相关概念:
熔断降级策略:
降级熔断代码配置
1 | DegradeRule rule = new DegradeRule(); |
流控规则控制台配置示例:
热点规则是对热点数据进行限流,支持对特定参数和参数的值限流。热点限流会统计参数中的热点参数,并根据配置的限流阈值与模式对包含热点参数的资源进行限流。
Sentinel利用LRU策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
热点规则的概念:
热点规则代码配置
1 | ParamFlowRule paramFlowRule = new ParamFlowRule("resourceName") |
流控规则控制台配置示例:
系统规则限流是从整体维度上去进行流控,结合应用的load,cpu使用率,总体平均RT,入口QPS和并发线程数等几个维度的监控指标来对总体的流量进行限流,在系统稳定的前提下保证系统的吞吐量
系统规则模式:
系统规则的概念
系统规则配置代码示例:
因为系统规则只对入口规则进行限定,所以需要将资源通过注解配置 @SentinelResource(entryType = EntryType.IN)
来指定为入口资源
1 | // 指定资源为入口资源 |
授权规则的作用是根据调用来源来拦截调用资源的请求,当不符合放行规则的请求过来就会被拒绝掉。
授权规则的概念
授权规则代码配置示例:
1 | AuthorityRule authorityRule = new AuthorityRule(); |
这里 limitApp 我设置的是 请求来源的ip 地址,这个ip地址是要我们手动去通过 ContextUtil.enter(resourceName, origin)
来设置的。
1 | public class MyInterceptor implements HandlerInterceptor { |
前文提到过 sentinel-spring-webmvc-adapter
依赖会提供一个将拦截器 SentinelWebInterceptor
, 源码为:
1 | protected String getResourceName(HttpServletRequest request) { |
它会解析一个请求为的请求地址为一个资源名,然后在sentinel控制台上就能看到各个请求的流控数据。但它没有提供请求适配各类流控规则的相关代码,
我们想要无缝的通过请求去适配各种流控规则还需要引入依赖:
1 | <dependency> |
然后注册一个过滤器:
1 |
|
接入 filter 之后,所有访问的 Web URL 就会被自动统计为 Sentinel 的资源,可以针对单个 URL 维度进行流控。
若希望区分不同 HTTP Method,可以调用 CommonFilter.init(FilterConfig filterConfig)
方法将 HTTP_METHOD_SPECIFY 这个 init parameter 设为 true,给每个 URL 资源加上前缀,比如 GET:/foo
这个包中一个重要类是 WebCallbackManager
许多限流配置都需要使用到这个类的api。
设置限流处理器:
1 | WebCallbackManager.setUrlBlockHandler((request, response, e) -> { |
设置url清洗器:
1 | WebCallbackManager.setUrlCleaner(new UrlCleaner() { |
设置请求来源名称,通过该配置我们在授权规则中就不需要通过ContextUtil.enter(resourceName, origin)
设置,而是通过一个自定义
的RequestOriginParser
直接指定请求的来源,也就是授权规则中的 limitApp
:
1 | WebCallbackManager.setRequestOriginParser(request -> { |
我们之前配置的流控规则都是存储在应用的内存中的,这种方式明显无法满足我们实际开发的需求,一旦项目被重启,流控规则就被初始化了,需要我们再次去重新配置,因此规则的持久化就显得很有必要了。
本节会介绍几类主流持久化方式并对自定义持久化做介绍
文件持久化是通过 sentinel spi 扩展点来加载本地文件中的持久化数据到内存中,它依赖接口 InitFunc
,对于非spring项目这种方式可以很便捷的实现
文件持久化。
实现文件持久化首先要自定义一个类并实现InitFunc
接口:
1 |
|
然后在resources 文件夹下新建文件 META-INF\services\com.alibaba.csp.sentinel.init.InitFunc
内容为MyflieInitFunc
的类路径:
1 | com.muggle.sentinel.config.MyflieInitFunc |
完成以上步骤后,文件持久化的方式就配置完成了。
InitFunc
的资源初始化方法 init()
并不是在项目启动的时候调用的,而是在首次产生流控数据的时候调用的,
也就是说它是一个懒加载的方法。
在文件持久化配置中,FileRefreshableDataSource
, FileWritableDataSource
, FlowRuleManager
这三个类是有必要去熟识的。
FlowRuleManager
源码分析1 |
|
该类的静态属性包括 流控规则数组 flowRules
,用于监控流控规则更新的监听器LISTENER
, 轮询监听流控配置的线程池SCHEDULER
,sentinel 配置类currentProperty
.
而它几个api也很明了,就是对流控规则的增删改查。
FileRefreshableDataSource
源码分析FileRefreshableDataSource
继承了AutoRefreshDataSource
,而AutoRefreshDataSource
中有一个线程池 service
用于拉取 文件中存储的规则
以及拉取间隔 recommendRefreshMs
.
:
1 |
|
我们重点关注 startTimerService
这个方法,这个方法是在构造器里面调用的,也就是说当你new 一个 FileRefreshableDataSource
时就会调用该方法
该方法就是通过线程池定时调用isModified
方法判断配置是否更新过,如果更新了就同步更新到父类属性 SentinelProperty
中,代码对应:
1 | AutoRefreshDataSource.this.getProperty().updateValue(newValue) |
不难判读出,父类抽象类的property
属性才是真正的获取规则提供拦截判断的关键属性。后文也会用到这个知识点,这里记一下。
我们可以看一下 FileRefreshableDataSource
构造函数:
1 |
|
不难看出,如果在 new FileRefreshableDataSource
时不指定刷新间隔就取默认值 3000 毫秒。
FileWritableDataSource
源码分析1 |
|
代码结构也很了然,一个数据转换器,一个 file 一个lock ,当框架调用 write
方法时上锁并往 file中写配置。
分析得差不多了,让我们看看实战效果吧;
首先启动项目和控制台,然后在控制台上配置一个流控规则,可以观察到项目规则存储文件中多了点内容:
文件中新增的数据:
1 | [{"clusterConfig":{"acquireRefuseStrategy":0,"clientOfflineTime":2000,"fallbackToLocalWhenFail":true,"resourceTimeout":2000,"resourceTimeoutStrategy":0,"sampleCount":10,"strategy":0,"thresholdType":0,"windowIntervalMs":1000},"clusterMode":false,"controlBehavior":0,"count":1.0,"grade":1,"limitApp":"default","maxQueueingTimeMs":500,"resource":"test","strategy":0,"warmUpPeriodSec":10}] |
我们重启项目和控制台规则也不会丢失,规则持久化生效。
通过分析我们知道,这种持久化方式是一种拉模式,胜在配置简单,不需要外部数据源就能完成流控数据的持久化。由于规则是用 FileRefreshableDataSource 定时更新的,所以规则更新会有延迟。
如果FileRefreshableDataSource定时时间过大,可能长时间延迟;如果FileRefreshableDataSource过小,又会影响性能;
因为规则存储在本地文件,如果需要迁移微服务,那么需要把规则文件一起迁移,否则规则会丢失。
文件持久化能应付我们需求的大部分场景,但对于微服务而言是不那么满足要求的;
因为文件持久化就必定要求我们在服务器上提供一个用于存储配置文件的文件夹,而微服务项目大部分情况是容器部署,这就让文件持久化显得不那么好用了。
为此,官方提供了自定义的持久化maven依赖:
1 | <dependency> |
以及在这个依赖的基础上开发的以CONSUL NACOS REDIS 作为数据源的maven 依赖:
1 | <dependency> |
以上三种种持久化不同于文件持久化,它们是推模式的,而且迁移部署起来更为方便,符合微服务的特性。接下来我们就以nacos持久化为例来学习一下这种方式是怎么配置的。
首先引入 nacos 相关依赖依赖:
1 |
|
然后通过FlowRuleManager
注册数据源就ok了
1 | ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, |
remoteAddress 是nacos 的地址; groupId和dataId均为nacos配置中心的属性,在创建配置项的时候由使用者自定义,如图为在nacos创建配置项的截图:
启动nacos,启动我们的项目和控制台,然后修改nacos中的配置项,就能再控制台上观测到规则变化,nacos中存储的规则也是json,我们可以把文件持久化教程中产生json
复制进去,这里就不在赘述。
这种模式是推模式,优点是这种方式有更好的实时性和一致性保证。因为我们和文件持久化比起来少注册了一个与FileWritableDataSource
对应的类,
也就是说应用中更新的规则不能反写到nacos,只能通过nacos读取到配置;因此我们在控制台上修改的规则也不会持久化到nacos中。这样设计是合理的,因为nacos作为
配置中心不应该允许应用去反写自己的配置。
因为文件持久化分析了一部分源码,因此这里不会对源码分析太多,只简单的介绍它是如何去读取到配置的。
1 |
|
我们看它的构造方法,创建了一个线程池,然后通过这个线程池 new 了一个nacos的Listener,Listener是一个监听器,initNacosListener() 方法是将监听器
注册到 nacos的configService 里面,通过这个监听器去监听nacos的配置变化,当配置发生更新的时候,调用监听器的 receiveConfigInfo
方法:
1 | public void receiveConfigInfo(String configInfo) { |
前面分析文件持久话我们就分析过,配置最终要被更新到父类的property
属性里面,再这里我们也看到了同样的代码。
sentinel 是阿里开源的流量控制,熔断降级,系统负载保护的一个Java组件;
Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
我们这里还是以springboot 项目写一个demo,创建完成springboot 项目之后pom中引入依赖:
1 | <dependency> |
然后定义流控规则并加载到内存:
1 |
|
然后创建 controller
并定义资源:
1 |
|
然后我们启动项目并访问 http://localhost:8081/test0
然后不断的刷新,就会发现如果刷新频率超过一秒就会返回error 否则会返回一个时间戳。
这里这些类的api和源码我们先不介绍,只对其功能先做一个大致的体验。
接下来我们继续引入依赖:
1 | <dependency> |
同时注入一个切面到springboot 中去:
1 | @Bean |
这个时候我们就可以通过注解去做流量控制了,写一个接口测试一下:
1 | @GetMapping("/test") |
同样通过浏览器访问这个接口并不断刷新,会发现会频率过快的时候会返回 springboot 的错误页面,这是因为当aop切面会抛出 BlockException
,当没有对应的
异常处理器的时候springboot就会返回默认错误页面。这个时候我们有两种方式处理我们超出访问频率的时候的逻辑。
第一种,加降级方法:
1 | @GetMapping("/test") |
第二种,加BlockException
异常处理器:
1 |
|
sentinel
还提供了 spring-mvc 的拦截器,配置该拦截器你可以对你项目的所有所有请求进行流控管理,首先我们需要引入依赖:
1 | <dependency> |
然后注入一个sentinel 的拦截器:
1 | @Configuration |
代码中 SimpleBlockExceptionHandler
是自定义流控异常处理器,作用是处理流控异常 BlockException
源码如下:
1 |
|
SentinelWebMvcConfig
是流控配置类,通过其属性命名就不难猜出其作用 isHttpMethodSpecify
是否区分请求方式;isWebContextUnify
是否使用统一web上下文; UrlCleaner
是url清理器,作用是对url进行整理
sentinel
为我们提供了一个控制台应用,通过这个控制台我们可以直观的看到流控数据,动态的修改流控规则,下面让我们看看如何接入控制台。
首先引入依赖:
1 | <dependency> |
这个依赖sentinel连接 控制端的通讯包。
然后添加配置:
1 | csp.sentinel.dashboard.server=localhost:8080 |
注意,因为这个配置项不是属于 springboot的 所以不能添加在application中,要通过 -D 的方式在jvm启动的时候添加这个配置项。
再去 sentinel的github 下载 控制台jar包 ,启动该jar包;访问8080 端口,
登录账号密码和密码都是 sentinel。这个控制台实际上是一个springboot应用,启动端口和账号密码都可以通过application 修改。
接下来,启动我们自己的应用,并访问一些接口,我们就能再界面上看到监控数据:
通过控制台我们可以监控流量,管理流控降级等规则,这些规则都是存储在我们程序应用的内存中的,因此我们还需要学会这些规则的配置使用及其持久化。
sentinel 官方还提供了 springcloud 的包,可以让我们很方便的在 spring cloud 项目中使用sentinel,springcloud 中使用 sentinel和 springboot
中使用sentinel方式差不多,只是多了一个链路调用;因此我们要先学会了如何在 springboot中使用它。
前文介绍了 kafka 的相关特性和原理,这一节我们将学习怎么在springboot中使用kafka;
首先导入依赖
1 | <dependency> |
springboot
有读取外部配置文件的方法,如下优先级:myisam 将整数型索引设置可为null的索引时会被变成可变索引
1、方式一
java -jar x.jar a b c 通过main(String[] args ) ,传入到args
2、方式二
java -jar x.jar -Da=111 -Db=222 -Dc=3333 通过 System.getProperty(“a”); 方式获取。作为环境变量
3、方式三
java -jar x.jar –a=111 –b=2222 是springboot支持的写法,可以通过@Value(“${a}”) 获取
maven dockerfile 打包
1 | mvn package org.springframework.boot:spring-boot-maven-plugin:2.3.5.RELEASE:build-info dockerfile:build |
指定settings
1 |
|
安装本地jar
mvn install:install-file -DgroupId=com.baidu -DartifactId=ueditor -Dversion=1.0.0 -Dpackaging=jar -Dfile=ueditor-1.1.2.jar
1 | -Dfile=/Users/lcc/IdeaProjects/dubhe-node/dubhe-node-provider/lib/ring/release/tdh-5.2/hadoop-annotations-2.7.2-transwarp-5.2.0.jar |
maven 推包到远程
1 | mvn -s "D:\data\maven\settings.xml" deploy:deploy-file -Dfile=libs/xxx.jar -DgroupId=com.qcloud -DartifactId=xxx -Dversion=1.1.1.Alpha -Dpackaging=jar -Durl=https://mirrors.cloud.xxx/maven-public/ -DrepositoryId=aaa |
docker
当 dockerfile-maven-plugin 插件版本冲突的时候 会报一个莫名其妙的异常:
1 | no String-argument constructor/factory method to deserialize from String value ('xxx') |
DOCKER_HOST:
1 | tcp://127.0.0.1:2375 |
设置之后报错:
1 | No connection could be made because the target machine actively refused it. |
原因:需要给2375端口加上守护进程
勾选 expose daemon on tcp://…….tls
nohup java -jar xxxx.jar >/dev/null 2>&1&
kafka +zk 的一个坑
配置文件中 listeners=PLAINTEXT:host 配置的地址是注册地址,也就是和客户端同一网络环境的地址,否则客户端无法找到kafka
zk中新特性(zookeeper 3.5 AdminServer) 占用8080端口,改log位置需要修改脚本
openjdk的两种包
一种是带devel 命名的,有tools,否则只有jre
mysql 清除表碎片
https://blog.csdn.net/weixin_34151001/article/details/113191032
mysql> alter table 表名 engine=InnoDB
mysql>optimize table test.t1;
kafka 的事务是从0.11 版本开始支持的,kafka 的事务是基于 Exactly Once 语义的,它能保证生产或消费消息在跨分区和会话的情况下要么全部成功要么全部失败
当生产者投递一条事务性的消息时,会先获取一个 transactionID ,并将Producer 获得的PID 和 transactionID 绑定,当 Producer 重启,Producer
会根据当前事务的 transactionID 获取对应的PID。
kafka 管理事务是通过其组件 Transaction Coordinator 来实现的,这个组件管理每个事务的状态,Producer 可以通过transactionID 从这个组件中获得
对应事务的状态,该组件还会将事务状态持久化到kafka一个内部的 Topic 中。
生产者事务的场景:
一批消息写入 a、b、c 三个分区,如果 ab写入成功而c失败,那么kafka就会根据事务的状态对消息进行回滚,将ab写入的消息剔除掉并通知 Producer 投递消息失败。
消费者事务的一致性比较弱,只能够保证消费者消费消息是精准一次的(有且只有一次)。消费者有一个参数 islation.level,这个参数指定的是事务的隔离级别。
它的默认值是 read_uncommitted(未提交读),意思是消费者可以消费未commit的消息。当参数设置为 read_committed,则消费者不能消费到未commit的消息。
kafka事务主要是为了保证数据的一致性,现列举如下几个场景供读者参考:
很多时候我们项目迭代到后期,项目会变得很混乱,往往只有少数人能知道某段代码是干嘛的和该如何去改,或者是干脆谁都不知道,只能靠通过注释去猜测这段代码可能的作用。原因有可能是因为团队内部的人事变动,导致原先写这段代码的人不再管理这段代码了,并且代码写的实在是屎没人捋的清。往往我们称这类代码为“祖传代码”,就像祖宗传下来的代码一样,没人懂没人敢动。祖传代码一多,这个项目就变成了屎一样,开发人员再这基础上迭代就如同屎海翻腾,恶心别人也恶心自己。这是一个很可怕的恶心循环,我们如何去避免这种事情发生呢?先让我们分析下这类代码的通病
我见过最长的方法是5000多行,那段代码没人敢动,只敢往下加 if else,每次需要改这段代码的开发都战战兢兢,生怕出现什么莫名其妙的bug。java 可是一门面向对象的语言,一个方法里面有5000多行可以说是很可恶的事情了。我想一开始代码长度可能没这么夸张,是什么导致这种结果的?一个是当初写这段代码的人本身写的是直来直去的方法,一堆if else ;后面迭代的开发,面对这么长的代码瞬间失去了从头读到尾的耐心,直接继续在后面加 if else 迭代,最后这个方法就变成了一个缝合怪一样的玩意。
好的 sql 可以很大程度上简化代码的复杂程度,但是太过复杂sql 本身就会给后来的开发人员造成阅读困难,结果又是变成一条无人敢动的祖传代码,我想这应该是不少公司极度抵制存储过程的原因之一。当然不少银行应用开发还是大量使用存储过程,存储过程有用武之地的,但是一个又臭又长的存储过程就等着变成祖传代码吧。当年我见到一个60多个join的sql,看到第一眼就惊为天人从此难以忘怀,当然那段sql也成了没人敢去动的代码了。
代码逻辑不明所以是我们开发很容易去犯的毛病,是一个不致命却烦人的毛病。在代码上的体现是,逻辑判断写的比较反人类各种双重否定是肯定,不把你绕晕不罢休。或者是写起代码来东一榔头西一棒槌,让人不知道你想干嘛。导致这个的原因有可能是开发人员在需求理解上出现偏差,做到后面发现不对劲,再回去改又不大可能了,只能硬着头皮往下写,结果就是代码弯弯绕绕;还有很重要的锅是在产品经理,任意变更需求,想一出是一出,开发人员无奈只能跟着想一出写一出。还用可能是开发人员方法或者类命名太艺术了,什么四川方言拼音这种没有十年脑血栓想不出的命名咱就不说了。就说那种国产凌凌漆式的无厘头命名——这看上去是个刮胡刀实际上是个吹风机,就这种不知道让人说什么好。
吐槽了一堆代码规范问题,接下来我们说说如何去规范我们的代码以及如何做到就算开发人员更换了,或者项目转手给他人了,仍然可以让后面的开发可以无碍的去阅读代码修改代码。当然各个公司/团队都有自己的一套代码规范,比如项目的结构、代码命名风格、代码格式等等。不同团队有不同的风格,但核心思想是大同小异的。接下来我就我个人的开发经验来分享一下一些代码规范的思想。
就我个人而言,这个理论是我代码规范中最浅显也是最核心的思想,只要稍微动动脑子就能想出这个思路出来。或许我们做业务开发的时候,大部分都在写crud,感觉似乎这部分代码没什么规范好说的,其实不然。对一段业务代码而言,我们可以将其分为四类:
大部分时候我们最关心的是逻辑判断相关的代码,其次是数据库交互,对于远程调用的方法,我们就视其为一个普通的方法以简化模型,方法调用算业务逻辑部分的代码,对于读代码的人而言基本上不关心数据校验和数据的转换(DTO转VO等)。因此,代码应该分出一个主次,应该尽量把主逻辑给凸显出来,最好一眼看去就能让人明白这个方法或者这个类干了啥,步骤是什么样的。对于那些不重要但必要的代码我称其为叶,对于那些主要的代码我称其为花。叶是为了衬托花的,因此我们应该将那些叶子代码精简或者隐藏起来。
隐藏叶子代码,突出主干逻辑的一些手法
1)Converter(转换器)
大部分时候我们使用 bean 拷贝使用的是 BeanUtils
这个类来完成,然而一些稍微复杂的实体转换,这个类就无法胜任了,这个时候我们只能手动的 get set ,往往就是这些get set 方法掩盖了主干逻辑,让代码结构不清晰。因此我建议在你的业务逻辑代码中引入 1)Converter
这个角色来专门负责数据的传递与转换。
2)manager 层
无论我们使用的持久层框架是哪一种,jpa 或者 mybatis 我觉得我们都应该对持久层的部分方法进行简单封装一下,这也是阿里规范里面提倡的。这样做好处是明显的,我们做一个查询时往往要 set 一些查询条件或者对查询结果进行一些简单的判断,往往这类操作在业务代码可能有比较高的重复性。如果把这些代码放到业务逻辑代码里面,少量还好,多了的话就显得很臃肿了。如果把这种代码移到manager层里面去,不仅主业务逻辑代码不会被干扰,还能提高一定的代码复用率。
3)方法简单封装
假设我们一个方法要完成一端逻辑要分成三大步,而每一个步骤又分成几个小步骤,那我们就可以将这个方法拆分成三个方法,然后在这三个方法里面完成各自的步骤。这手法是很简单的,想必大家都能想到,但是我这里要介绍的是简化复杂方法封装的神器——函数式编程,我这里指的函数式编程不仅仅是 stream 流和 lambda 表达式的使用。函数式编程封装适用的场景是:整个流程比较固定,但是某几个步骤变化是不确定的。我们可以去看看 java.util.function
这个包的源码,你会发现这个包下面全是接口,这些接口被称为函数式接口。这些函数式接口总体上分为四类:
以 Consumer 的使用为例:
1 | public User getUser(Consumer<User> consumer){ |
函数式编程的想象空间很大,使用的得当必定会简化你的代码,提高代码复用率。但是在多线程中使用函数式要留意数据的可见性问题。
1)日志
首先我们要明白日志是给人看的,你加这段日志时要考虑清楚,有没有人会去查这段日志,这段日志有没有用。然后我们查阅日志的时候,一般会通过关键词去搜索;因此我们打的日志一定要有关键词,而且这个关键词不要和其他日志重复,不要过长,便于搜索才是王道。大部分情况我们查看日志都是为了追溯bug,那么一个基本原则就是能通过日志分析出业务逻辑或者流程的走向,对此我建议打日志的地方:
并不是所有的这些地方都应该打上日志,有的时候我们可能只需要通过一两条日志就能分析出整个流程的问题点在哪,这个时候其他的日志就显得多余了。还有我们打完日志之后应该在本地环境追溯一下,看看这些日志自己是否能读懂,是否有必要,是否少了重要参数。
2)注释
最基本的两个注释——类注释,方法注释相关规范阿里开发手册上就有,我这里就不复述了,我分享下我写注释的个人习惯。
方法注释上除了基本的注释,我还会将产品需求的原文贴重要的部分上去再写上日期,这样做的好处是让别人明白产品需求要求干啥这个方法该干啥,而且产品经理偷偷改需求你还能有追查的根据,有个小本本偷偷记录他的罪行。
代码注释我分享一个我偷师来的小技巧:
1 | pulic void test(){ |
如你所见,对于主干的步骤 我用 /** 1. */
/** 2. */
javadoc的注释来标注了,而普通的注释我用 //
标注,因为idea 在纯黑主题下会给 /**
这样的注释配上绿色,会比较显眼。我通过这种方式来强调我代码那些是花,哪些是叶子。当然这种方式实际上是不大符合代码规范的,小伙伴们理性取舍,这种手法未必好。
对于面向对象的的语言,六大基本是很重要的开发准则,但似乎大部分人在写代码的时候都不大在意这个,这也是导致一个方法变得又臭又长的一个重要原因之一。对于类的复杂度我们应该遵循单一职责原则——一个类或者方法承担的职责越多,它被复用的可能性就越小,重构或者修改起来就会变得困难重重,我们应该尽量让一个方法只去做一件事情。
对于许多代码我们只要通过一些简单的手法就能很好的提高其扩展性,比如通过接口去实现类与类之间的协作就能提前解决掉许多未知隐患,而且运用得当的情况下还能满足开闭原则与里氏替换原则,其实service层的设计就有那么点味道了,而且spring的特性也支持接口注入List和map,然而许多开发多年的同学都不知道这个特性,这个特性在许多场景下可以提高代码的扩展性,众所周知,map可以减少代码的 if else 分支。
很多时候,好的方法命名本身就是对代码的一种注释,我这里好的方法命名是指大家约定俗成的命名规则。如果你多留心各个开源框架的代码都会发现一些特定的命名规则。阿里开发手册里面也列举不少命名前缀与后缀的规范,其实各个团队可以根据自己的实际情况规定一些命名规则,降低团队内部的代码阅读的成本。关于我的文章 设计模式杂谈
介绍过部分命名规则,感兴趣的小伙伴可以去看看。
正确代码提交日志格式可以帮助开发人员及时的缕清代码的修改历史,从而快速的定位问题。以git为例,我们大部分人提交日志就是几个字而已,当然你能够通过日志去定位到自己的修改历史的话,这样做也没什么大问题,但是对于团队而言,你的修改日志要让别人能看懂就得按一定的格式来写了。Git Commit message的 Angular规范中定义的 commit message 格式有3个内容:
这里由于篇幅问题不细说,感兴趣的小伙伴可以百度查查资料。我们团队不一定要按照这么严格的规则来,但是可以制定一个类似的规范来管理提交日志。
对于团队而言,gitflow 是一个很不错的开发流程。可以很大程度上管理好我们的分支代码,避免团队的人由于误操作而导致某个重要分支出现问题。下面贴出gitflow 流程图,对于其具体内容同样不会介绍太多,感兴趣小伙伴去百度吧
本节主要介绍提高代码质量的idea插件和框架,当然大名鼎鼎的 阿里代码规范插件咱就不介绍了,想必大家多少了解。不过本人感觉这个插件并不适合一些团队,一是感觉这个规范太过严格,对开发人员素质要求太高,二是有的团队有自己的规范规则,而且有可能和阿里规范冲突,不适用于这个插件。下面介绍的插件可能不适合一些小伙伴。我列举出来大家自己寻思吧。
对于我而言是很喜欢这个东西的,这个框架解决的问题其实就是我上文提到的花叶论中的 “数据转换” 的问题。其实不少公司也有类似的概念——定义一个工具类作用是将 DO转VO 或者 VO转DTO等,一般这类类都是以 converter
结尾。而mapstruct这个框架通过编译期生成字节码来自动的生成bean的转换类。我们想将一个bean的数据赋值给另外一个bean只需要去定义接口即可。这样既减轻了开发人员的工作量还将无意义的get和set方法从逻辑代码块中剔除出去。这个框架的缺点是字节码缓存问题,用过类似自动生成字节码工具的小伙伴应该知道——mapstruct 是根据接口去自动生成类的,当我们更新了接口的时候,这个类有可能没重新生成,当然这只有用idea调试的时候才会有的问题,所以也不必太担心。
idea checkStyle 插件可以通过自定义配置文件来统一团队的代码风格和代码规范,降低团队的交流成本,一般配合 save actions Reborn 食用更佳。关于checkStyle的配置文件网上也不少,这里也不贴出来占篇幅了。
前文提到过git flow 给团队带来的好处,idea也有对应的插件——git Flow Integration,可以通过这个插件来规范我们的流程:
开发新功能选择 start Feature 拉取分支,修复bug 选择 Start Bugfix 拉取分支,等等。此外还有 push on finish等功能,小伙伴如果感兴趣可以百度。
这个主要是用来规范git commit 的一个idea插件小工具了,github上也有类似的开源插件。团队内部也可以自己开发一个类似插件,比较简单,成本也不高。
代码规范的一些个人看法就聊到这了,喜欢的小伙伴可以分享一下哦。
消息队列一般包含两种模式,一种是点对点的模式,一种是发布订阅的模式。前文提到过 kafka 是一款基于发布订阅的消息队列。
那么kafka是怎么去发布消息,怎么去保存消息,订阅消息的呢?首先我们从kafka的发布订阅模型开始分析。
kafka 是一款基于发布订阅的消息系统,Kafka的最大的特点就是高吞吐量以及可水平扩展,
Kafka擅长处理数据量庞大的业务,例如使用Kafka做日志分析、数据计算等。
ListenerContainer
的使用在消费端,我们的消费监听器是运行在 监听器容器之中的( ListenerContainer
),springboot 给我们提供了两个监听器容器 SimpleMessageListenerContainer
和 DirectMessageListenerContainer
在配置文件中凡是以 spring.rabbitmq.listener.simple
开头的就是对第一个容器的配置,以 spring.rabbitmq.listener.direct
开头的是对第二个容器的配置。其实这两个容器类让我很费劲;首先官方文档并没有说哪个是默认的容器,似乎两个都能用;其次,它说这个容器默认是单例模式的,但它又提供了工厂方法,而且我们看 @RabbitListener
注解源码:
这一章节我们会学习rabbitMQ在项目生产中一些重要的特性,如持久化,消息确认机制,消息过期等特性。只要能利用好这些特性,我们就能开发出可用性强的,功能强大的MQ系统。
more >>从这一节开始我们进入rabbitMQ的实战环节,项目环境是spring-boot 加maven。首先让我们创建一个spring-boot项目,然后引入web依赖和 rabbitMQ的依赖
more >>前文我们学习了 MQ的相关知识,现在我们来学习一下实现了AMQP协议的 rabbitMQ
中间件。rabbitMQ 是使用 erlang 语言编写的中间件(erlang之父 19年4月去世的,很伟大一个程序员)。
对 rabbitMQ 我们已经有了初步的了解,现在我们来安装 rabbitMQ 来进行一些操作。因为大部分人的操作系统都是windows 而且作者本人使用的也windows系统。所以这里只介绍在windows上安装rabbitMQ。mac用户自行解决(仇富脸)。
more >>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
-XX:+PrintGC
输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
一、jps:虚拟机进程状况工具
二、jstat:虚拟机统计信息监视工具
三、jmap:Java内存印象工具
四、jhat:虚拟机堆转储快照分析工具
五、jstack:Java堆栈跟踪工具
六、jinfo:Java配置信息工具
集成式的VisualVM和jConsole
最近在做一个项目,这个项目很有特点——它是一个分布式项目但是它却未使用分布式事务。我分析其事务机制和缺陷时,突然灵感一来,于是有了这篇文章。
在讨论分布式事务之前,我们先把spring事务传播机制过一遍,文章参考自事务传播行为详解 这位大佬写的很用心,文末评论区还讲到了一个关于spring事务的一个很重要的特性。spring事务传播行为有七种:
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
为了大家便于理解方便阅读我对原文做了总结,在这里我们讲三种事务的场景:
下面根据参考文章中的例子来一一说明
代码示例:
1 |
|
上述代码中user1Service.addRequired(user1)
是一个 “正常” 的 insert 事务,user2Service.addRequiredException(user2)
是一个抛异常的事务。这三个方法都会发生回滚。以test3()
为例, user1Service.addRequired(user1)
正常提交后外部的事务(test3()
所在的事务 )发生了回滚,这个事务也会跟着回滚,这便是包含关系。
挂起性事务,是外部事务和内部事务互不干扰,两者只能通过抛出异常来交互(后面分布式项目分析中就是和这种事务一样)。示例:
1 | @Transactional(propagation = Propagation.REQUIRED) |
上述代码中 user1Service.addRequired(user1)
和外部事务是包含关系;user2Service.addRequiresNew(user2)
是挂起事务;user2Service.addRequiresNewException(user3)
是挂起并将会执行失败的事务。test1 方法中由于外部事务是包含事务,因此user2Service.addRequiresNewException
异常会导致 user1Service.addRequired
回滚 而由于user2Service.addRequiresNew
是挂起事务它不会回滚。在test2 中由于 test2 捕获了异常所以不会触发外部事务的回滚,user1Service.addRequired
和 user2Service.addRequiresNew
均能执行成功;但是要注意一个情况,假如 user2Service.addRequiresNewException(user3)
这个方法加了包含性事务的注解(既 @Transactional(propagation = Propagation.REQUIRED)
) 的情况下,虽然在外部事务中try catch 了,但其方法本身所在的事务发生了回滚,该子事务回滚之后会将整个事务标记位记为rollbackOnly
,当外部事务发现事务被标记为 rollbackOnly
时不会提交,而是回滚。
该事务如果内部事务回滚,不会触发外部事务的回滚,但外部事务的回滚会导致内部事务回滚。下面看示例:
1 | @Transactional |
在test1 中外部事务回滚,导致嵌套事务回滚,user1Service.addNested
和 user2Service.addNested(user2);
均回滚了,在test2 中,user2Service.addNestedException(user2)
这个嵌套事务回滚了,但外部事务不会回滚 因此 user1Service.addNested(user1)
不会回滚。在挂起性事务中我们提到rollbackOnly
标志位,在test2里也是一样,如果user2Service.addNestedException(user2)
是一个抛异常的包含性事务,其外部事务也会回滚,既 user1Service.addNested
会回滚。
为了下文能便于理解,我们先做个简单的总结,其实spring事务按顺序排下来就分四种情况:
假设我们在分布式系统中使用普通的本地事务会怎么样呢(作者运气比较好,不需要假设,实际场景就是)? 下面我们来分析一段伪代码
1 |
|
rpcService.update()
是一个更新数据库的rpc方法,localService.update()
是一个更新数据库的本地方法。为了简化模型,我们认为两个方法操作的是同一个数据库。这个方法执行会发生什么呢?很明显,这里相当于一个挂起性事务,rpcService都夸虚拟机了,自然不会被本地spring的事务所管控,相当于两个事务放在一起互不相干。该方法将会导致,rpcService.update()
写入脏数据,而 localService.update()
会回滚,这是很糟糕的情况。如果rpcService.update()
抛出异常还好,还能让事务回滚,要是正常执行就完犊子了。我们要怎么去避免呢?最笨的办法是 我们调用rpc的service时只执行查询语句。所有更新数据库的操作全部在本地执行。但这种方式不是任何情况都适用,有的时候我们不得不去 rpc update。那么在没有引入分布式中间件的时候怎么去实现一个分布式事务?一种方式是通过 mysql的 XA
分布式事务机制,这种方式缺点也是很明显的,首先 XA
在5.6以上版本才适用,其次它很耗资源。我们考虑一下有没有通过代码或者结构设计的方式来实现数据一致。我们可以要 rpcService.update()
的事务卡在那不commit,等localService.update()
commit 了再让它commit,这样又有问题了,如果卡住 rpcService.update()
的事务那么,这个方法就会阻塞,只能开启异步线程来让它在后台挂起,异步又会导致该方法必须是void的,这又是很难做到的事情——必须要所有rpc方法都无返回值。
上面几种思路貌似都不是很理想,虽然能实现但效果必定不会很好,有没有别的办法?我们回过头来分析开始那段伪代码,这段代码存在的问题是如果本地方法回滚了,rpc的方法会产生脏数据。那如果脏数据能在后续步骤清除并且这部分脏数据不会影响正常业务呢?我可以在本地方法rollback的时候清除它,而且正常的业务代码也不会被它影响。我们都知道不少企业在设计数据库的时候,对数据的删除不是使用物理删除,而是使用逻辑删除,被逻辑删除的数据也不会影响正常业务的运行,而被删除的数据实际上还保存在数据库,只是将删除标识标记为1,表示已删除。
基于上述原理,我们可以整个逻辑提交这个概念——在数据库中专门准备一个字段commited
0 表示虽然写入库,但是未被commit 属于“脏数据”,1 是已经commited 可以被业务代码读写的数据。那上面的代码也要改一下:
1 | @Transactional |
上述设计方式,似乎解决了我们的问题,但只是理想状态下不会出错;不理想的状态下,可能发生网络波动,rpcService.rollBack()
请求未抵达抛出异常,那么数据库里面会堆积不少脏数据,虽然对业务没影响,但是很影响性能。而且如果有多个commit,比如这样:
1 | @Transactional |
假设rpcService.commit2()
发生网络波动,未发出请求抛出异常,那么会发生commit0
成功 commit1
成功,本地事务失败,commit2
失败。这样就又产生影响业务的脏数据了。这种情况证明办?我们需要一个机制来能在commit或者rollback请求未发送出去的时候去重试,保证能够发送请求出去。因此我们要保证 commit 或者 rollback 不能抛异常,并且能够去请求失败的时候重试。重试很好实现,做一个标志位和计数器当请求成功的时候改变标志位状态,计数器计数重试次数,超过次数就通过某种机制来通知到运维人员需要去检查什么地方出了问题,手动对数据提交或者回滚。
推导到这一步好像这个思路要做的事情还比较多了,而且这个功能通用性也挺强,要不整成中间件吧,这个中间件有重试机制,错误通知机制;物理commit,逻辑commit ,逻辑rollback的请求接口。对于这三个接口的注册方法我们可以用注解或者实现接口的方式,通过aop来获取其注册到这个中间件的接口。然后我们需要使用分布式事务的时候先从中间件拿到这个事务的三个接口,而事务的执行方提供这三个接口。
嗯,这个套路研究到这里好像还蛮牛掰的,要不咱们取个响亮点的名字吧——就叫TCC好了
责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链模式也叫职责链模式。
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,所以责任链将请求的发送者和请求的处理者解耦了。
more >>策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。策略模式有以下优点:
代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
代理模式的主要优点有:
代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
代理对象可以扩展目标对象的功能;
代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
其主要缺点是:
在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
增加了系统的复杂度;
组合(Composite)模式的定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系。组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
more >>装饰器(Decorator)模式指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。采用装饰模式扩展对象的功能比采用继承方式更加灵活;可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。但是装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。
more >>建造者模式(Builder)是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式属于对象创建型模式。我们获得一个对象的时候不是直接new这个对象出来,而是对其建造者进行属性设置,然后建造者在根据设置建造出各个对象出来。建造者模式又可以称为生成器模式。
more >>单例模式 (Singleton Pattern)使用的比较多,比如我们的 controller 和 service 都是单例的,但是其和标准的单例模式是有区别的。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
more >>桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要4个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:
对于有两个变化维度(即两个变化的原因)的系统,采用第二种方案来进行设计系统中类的个数更少,且系统扩展更为方便。第二种方案即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。对于有两个变化维度(即两个变化的原因)的系统,采用桥接模式开发更为方便简洁。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
more >>简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
简单工厂模式包含如下角色:
1 | public static void main(String[] args) { |
这个XmlBeanFactory
便可以看做是一个稍微变形的简单工厂,getBean()
方法便是获取产品的实例方法,userBean
便是我们的产品。如果我们以后遇到与spring中XmlBeanFactory
类似场景我们便可依瓢画葫芦写出一个漂亮的简单工厂。
也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
工厂方法模式包含如下角色:
java.util.Collection接口中定义了一个抽象的iterator()方法,该方法就是一个工厂方法。
我们来看看ArrayList
中的iterator()
实现
1 | @NotNull public Iterator<E> iterator() { |
它new了一个ArrayList
的内部类Itr
然后将其返回,Itr:
1 | private class Itr implements Iterator<E> { |
这里ArrayList
对Iterator
来说就是一个工厂类,它的iterator()
方法便是生产Iterator
的工厂方法。
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。
抽象工厂模式包含如下角色:
我们可以看到 抽象工厂和工厂方法的区别是——抽象多了生产相关联产品的其他方法。可以理解为对工厂方法的一个升级,我们来看HashMap
这个类:
1 | HashMap<Object, Object> objectHashMap = new HashMap<>(); |
这里HashMap
就是抽象工厂,它的values()
和entrySet()
就是两个工厂方法,Collection<Object>
和Set<Map.Entry<Object, Object>>
是产品。注意:抽象工厂中抽象的含义是对产品的抽象,不再是某个产品,而是某系列产品,工厂模式类命名一般以factory
结尾。
从官网 https://apache.org/dist/zookeeper/zookeeper-3.5.5/ 上下载zk(注意windows也是下载 tar.gz后解压),./conf下有个zoo_sample.cfg
复制到同目录下改名为zoo.cfg
,在目录下新建data和log文件夹,修改zoo.cfg中的 dataDir 和 dataLogDir
为 data和log的路径。现在启动zk,在bin目录下有个zkServer.cmd
,运行启动。启动ZK客户端对ZK进行简单的读写操作,在bin目录下打开cmd,运行:
1 | ### 初始化git 仓库 |
1 | ### 查看分支 |
1 | ### 对受保护分支无法使用强推,强推解决多仓库不同源问题 |
1 | ### 多仓库远程推送 |
git subtree 是在当前仓库下创建子目录,适用于多仓库间公共代码的维护
1 | ### 添加子仓库 |
HEAD的含义:代表当前仓库最新版本。HEAD^
和HEAD~
的意义和区别HEAD^+数字表示当前提交的父提交。具体是第几个父提交共同过^+数字指定,HEAD^1第一个父提交,该语法只能用于合并(merge)的提交记录,因为一个通过合并产生的commit对象才有多个父提交。HEAD~
(等同于HEAD^,注意没有加数字)表当前提交的上一个提交。
如果想获取一个提交的第几个父提交使用HEAD^+数字,想获取一个提交的上几个提交使用HEAD~
。HEAD^
和HEAD~
或HEAD^^
和HEAD~~
并没有区别,只有HEAD^+数字才和HEAD~有区别。
Gradle是一个构建工具,定位和maven一样,用于管理项目依赖和构建项目。和maven比起来的优势是:语法更灵活,更方便管理项目(个人很讨厌XML)。
gradle具有以下特点:
注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
1 | public class BootStaterTestApplicationTests { |
解中属性可以有默认值,默认值需要用 default 关键值指定。
1 | @Target(ElementType.LOCAL_VARIABLE) |
一些缺省写法略
类注解
1 | test test = new test(); |
属性注解
1 | Field[] fields = aClass.getFields(); |
方法注解
1 | Method[] methods = aClass.getMethods(); |
代理是一种软件设计模式,这种设计模式不直接访问被代理对象,而访问被代理对象的方法,详尽的解释可参考《java设计模式之禅》,里面的解释还是很通俗的。给个《java设计模式之禅》下载地址:https://pan.baidu.com/s/1GdFmZSx67HjKl_OhkwjqNg
在JDK中提供了实现动态代理模式的机制,cglib也是一个用于实现动态代理的框架,在这里我介绍jdk自带的动态代理机制是如何使用的。先上代码再慢慢解释:
1 | // 定义一个接口,接口中只有一个抽象方法,这个方法便是将要被代理的方法 |
1 | // 定义 一个类实现这个方法,方法里写上自己的逻辑。 |
1 | import java.lang.reflect.InvocationHandler; |
1 | public static void main(String[] args) { |
输出结果:
1 | 开始执行动态代理 |
我们知道spring 的AOP是通过动态代理实现的,现在让我们好好分析一下动态代理,示例中定义了一个接口 Subject,一个继承接口的SubjectImpl类,一个实现了InvocationHandler的MyProxy类,并调用了Proxy.newProxyInstance方法。Subject定义了将要被代理执行的方法,SubjectImpl是被代理的类(雇主),MyProxy类是代理执行的类(跑腿的),它的invoke(Object proxy, Method method, Object[] args)方法便是实际被执行的方法,它的第一个参数proxy作用:
- 可以使用反射获取代理对象的信息(也就是proxy.getClass().getName())
- 可以将代理对象返回以进行连续调用,这就是proxy存在的目的,因为this并不是代理对象(MyProxy虽然是代理的类,但代理对象是 Proxy.newProxyInstance方法生成的。)。
method 是被代理的方法类型对象,args是方法的参数数组。通过Proxy.newProxyInstance生成代理类后就可以执行其中的代理方法了。
如果我们直接执行SubjectImpl.test()方法则只返回一个字符串,但使用动态代理我们可以在方法执行前和执行后加上自己的逻辑,这样大大提高了代码的复用性;想一想,如果你写了一堆方法,方法里很多代码是一样的,这样的代码是不是很丑?好,现在你把重复的代码单独抽出来做一个方法,但这样你的每个方法都被写死了,和那个公共的方法耦合在一起,这样很不灵活。如果我突然想一部分方法的公共方法是a(),一部分方法的公共方法是b(),那改起来很麻烦,扩展性很差。使用动态代理就很好的解决了这个问题,被代理对象可以任意指定,代理的逻辑可以任意实现,二者互相独立互不影响,并且可以由客户端任意进行组合,这就是所谓的动态。
在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。对象引用被划分成简单的两种状态:可用和不可用。从JDK 1.2版本以后,对象的引用被划分为4
种级别,从而使程序能更加灵活地控制对象的生命周期,引用的强度由高到低为:强、软、弱、虚引用。
对象生命周期:在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。
more >>分为20个模块
core container
core beans context expression language
spring
beanFactory
bean被当做一种资源,各个factory完成对bean的增删改查,注入读取,初始化等功能
inputStreamSource 封装inputstream
factoryBean :
spring的标准实例化bean的流程是在xml中提供配置信息然后读取配置信息。factoryBean通过实现接口的方式实例化bean(java代码配置bean信息)
demo
单例在spring同一个容器中只会被创建一次,后续直接从单例缓存中获取
循环依赖
objectFactory
singletonFactory
创建bean
实例化前置处理器
实例化后置处理器
循环依赖
检查是否循环依赖
循环依赖会导致内存溢出
构造器循环依赖
支持用户扩展
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
组件: 1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供 作用:接收请求,响应结果,相当于转发器,中央处理器。有了dispatcherServlet减少了其它组件之间的耦合度。 用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。
2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供 作用:根据请求的url查找Handler HandlerMapping负责根据用户请求找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。
3、处理器适配器HandlerAdapter 作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler 通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。
4、处理器Handler(需要工程师开发) 注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。
5、视图解析器View resolver(不需要工程师开发),由框架提供 作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。 一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。
6、视图View(需要工程师开发jsp…) View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf…)
核心架构的具体流程步骤如下: 1、首先用户发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制; 2、DispatcherServlet——>HandlerMapping, HandlerMapping 将会把请求映射为HandlerExecutionChain 对象(包含一个Handler 处理器(页面控制器)对象、多个HandlerInterceptor 拦截器)对象,通过这种策略模式,很容易添加新的映射策略; 3、DispatcherServlet——>HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器; 4、HandlerAdapter——>处理器功能处理方法的调用,HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView 对象(包含模型数据、逻辑视图名); 5、ModelAndView的逻辑视图名——> ViewResolver, ViewResolver 将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术; 6、View——>渲染,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术; 7、返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束。
下边两个组件通常情况下需要开发:
Handler:处理器,即后端控制器用controller表示。
View:视图,即展示给用户的界面,视图中通常需要标签语言展示模型数据。
WebApplicationContext
ServletContext
ApplicationContext
问:
我们可以通过
ApplicationContext ap = new ClassPathXmlApplicationContext(“applicationContext.xml”);
得到一个spring容器,那么在传统ssm项目中是如何。。知道了
实例化bean对象(通过构造方法或者工厂方法)
设置对象属性(setter等)(依赖注入)
如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。(和下面的一条均属于检查Aware接口)
如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身
将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法
调用Bean的初始化方法
将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法
使用Bean
容器关闭之前,调用Bean的销毁方法
git subtree可将多个git项目合并在一起,可解决protobuf更新的问题;
打包maven私有仓库也可行,但是maven私有仓库不适合频繁更新,而protobuf更新会很频繁。
1 |
|
每次开机都启动一堆软件,很麻烦,该肿么办?
写个批处理文件 步骤(这里以启动微信为例 ):
start “xx” “xxx”
xx 代表程序名称,可以随便起;xxx代表你想启动的程序的位置 获取程序位置的方法:
more >>这篇博客会记录我的一些刷题心得
两数之和
给定一个整数数组 nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
more >>原文链接 http://www.ruanyifeng.com/blog/2018/07/cap.html
分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。
Consistency 中文叫做”一致性”。意思是,写操作之后的读操作,必须返回该值。
Availability Availability 中文叫做”可用性”,意思是只要收到用户的请求,服务器就必须给出回应。
Partition tolerance 中文叫做”分区容错”。大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。
这三个指标不可能同时做到,一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。对于Eureka而言,其是满足AP的。
作者:muggle
传统的IO又称BIO,即阻塞式IO,NIO就是非阻塞IO了,而NIO在jdk1.7后又进行了升级成为nio.2也就是aio;
Java IO的各种流是阻塞的。这意味着,当一个线程调用read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
作者:muggle
服务治理是微服务架构中最为核心和基础的模块。它主要用来实现各个微服务实例的自动化注册与发现。随着服务的越来越多,越来越杂,服务之间的调用会越来越复杂,越来越难以管理。而当某个服务发生了变化,或者由于压力性能问题,多部署了几台服务,怎么让服务的消费者知晓变化,就显得很重要了。不然就会存在调用的服务其实已经下线了,但调用者不知道等异常情况。这个时候有个服务组件去统一治理就相当重要了。Eureka便是服务治理的组件。
more >>作者:muggle
共五种:
共七种:
共十一种:
设计模式的最终目的是为了实现代码设计的六大基本原则的,我们在使用设计模式的时候千万要记住这一点,不用为了使用设计模式而去强行套设计模式
不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。
当需求变化时,将通过更改职责相关的类来体现。如果一个类拥有多于一个的职责,则多个职责耦合在一起,会有多于一个原因来导致这个类发生变化。一个职责的变化可能会影响到其他的职责,另外,把多个职责耦合在一起,影响复用性。
单一职责原则解决的问题:
- 降低类的复杂度;
- 提高类的可读性,提高系统的可维护性;
- 降低变更引起的风险(降低对其他功能的影响)。
任何基类可以出现的地方,子类一定可以出现。
只有当子类可以替换掉父类, 代码功能不受到影响时,父类才能真正被复用, 而子类也能够在父类的基础上增加新的行为;从而达到代码复用与扩展的目的;里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
里氏替换原则解决的问题:
- 增强程序的健壮性, 版本升级时也可以保持非常好的兼容性。
- 提高代码复用率
这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
对于引用的模块尽量去扩展,而不是去修改它,也就是所谓的对扩展开放对修改关闭。开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则,抽象化是开闭原则的关键。
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。 根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可
一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
首先允许我提一点建议,有不少设计模式的教程喜欢拿生活中一些事物举例子,比如什么什么奥迪汽车,宝马,汽车工厂这种。这种方式确实能让人更加直观的的体会到一个设计模式的用法和作用,但是我以前这样学的后果就是——看啥都像设计模式,想往设计模式里面套又套不出个所以然来。实际上有的设计模式结构是由好几个角色组成,该如何去写怎么命名都是确定的有理有据的,你光记住生活中的一个例子然后平时写代码去套这个例子,你会有种无从下手的感觉。我们应该把每个设计模式的使用场景结构都记住,但是这样死记难记不说,还很难做到活学活用。我的的建议是结合源码去记,这样你能根据源码中的例子依瓢画葫芦写出自己想要的设计模式出来,我接下来对设计模式的讲解也是结合源码进行讲解的。
然后,我个人建议是能不用设计模式的地方就不用设计模式。并不是因为设计模式不好,一个设计得好的设计模式确实可以减少我们工作量和代码维护成本,但是一个设计不好的设计模式使用起来代价巨大。要知道设计模式的最终目的是减少我们的工作量,不要为了设计模式而设计模式。一个垃圾的设计模式的缺点:代码维护成本翻倍,因为会凭空多了好多莫名其妙的类;代码读不懂,你设计模式用的乱七八糟,别人只会感觉你的代码毫无逻辑,绕来绕去。
一个好的设计模式,首先要用对场景,然后命名要规范,要让别人一看命名就知道你用的什么设计模式,然后根据设计模式去找相关联的类,这样别人脑海里才能形成一个脉络,有点没法按设计模式规范命名的地方也请加上注释,不然让别人猜你的代码写了些啥,用的啥设计模式吗?
muggle
类从被加载到虚拟机内存中内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(loading)、验证(verification)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(using)卸载(unloading)七个阶段。其中验证、准备、解析三个阶段统称为连接(linking)。
加载是类加载机制的第一个阶段,在这个阶段,虚拟机做了三件事情:
- 通过类的全限定名来获取定义此类的二进制字节流;
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义;然后在内存中实例化一个Class类的对象,加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始了。
验证是连接阶段的第一步,这一阶段目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上来看,验证阶段包括以下四个动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备阶段是正式为类变量分配内存并设置类变量初始值得阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念——1.这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,2.这里初始值是数据类型的零值,假设一个类变量定义为 :
1 | public static int value=2; |
那变量value在准备阶段的值为0而不是2.
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段可以通过自定义类加载器参与之外,其余动作都是虚拟机控制的。到了初始化阶段,才真正的执行java代码。初始化阶段是执行类构造器
想要使用一个类,必须对其进行初始化,但初始化过程不是必然执行的;jvm规范中规定有且只有以下五种情况必须对类进行初始化:
- 遇到new、getstatic、putstatic、invokestatic这四个字节码指令的时候,如果类没有进行初始化,则需要先触发其初始化。生成这四条指令最常见的java代码场景是:使用new创建对象、读取或者设置一个类的静态字段(不包括值已在常量池中的情况)、调用一个类的静态方法的时候;
以上五种情况称为对一个类进行主动引用;其他引用类的方式都不会触发初始化,称为被动引用。下面举一个被动引用的例子:
1 | public class TestClassloading { |
输出结果:
1 | 父类被初始化 |
显然,子类没有被初始化,这里SubClass.number为被动引用,不会对子类初始化。
通过一个类的全限定名来获取描述此类的二进制字节流这个动作被放到虚拟机外部区实现,以便让应用程序自己决定如何去获取所需的类,实现这个动作的代码模块称为类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。也就是说比较两个类是否相等必须要类加载器和类都相等。
从java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器是虚拟机的一部分;另一种就是java代码实现的独立于虚拟机外部的类加载器,这种类加载器继承类抽象类java.lang.TestClassloader。
类加载器还有一个很重要的概念就是双亲委派模型——在类加载器工作的时候是多个类加载器一起工作的它们包括:扩展类加载器,应用程序类加载器,启动类加载器,自定义类加载器。类加载器的层次图如图:
类加载器层次结构
双亲委派模型的工作流程是如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求交给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会被交给启动类加载器,当父类反馈无法加载这个类的时候,子类才会进行加载。
标记-清除算法是最基础的算法,算法分为标记和清除两个阶段,首先标记出要清除的对象,在标记完后统一回收所有被标记的对象,标记方式为j《jvm系列之垃圾收集器》里面所提到的。这种算法标记和清除两个过程效率都不高;并且在标记清除后,内存空间变得很零散,产生大量内存碎片。当需要分配一个比较大的对象时有可能会导致找不到足够大的内存。 more >>
java内存在运行时被分为多个区域,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程生成和销毁;每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,在这几个区域内就不需要过多考虑回收问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而堆区就不一样了,我们只有在程序运行的时候才能知道哪些对象会被创建,这部分内存是动态分配的,垃圾收集器主要关注的也就是这部分内存。
more >>作者:muggle
在语言层面上,创建一个对象通常是通过new关键字来创建,在虚拟机中遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过;如果没有的话就会先加载这个类;类加载检查完后,虚拟机将会为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,在堆中为对象划分一块内存出来。
虚拟机给对象分配内存的方式有两种——“指针碰撞”的方式和“空闲列表”的方式。如果java堆内存是绝对规整的,所有用过的内存放在一边,未使用的内存放在另一边,中间放一个指针作为指示器,那分配内存就只是把指针向未使用区域挪一段与对象大小相等的距离;这种分配方式叫指针碰撞式,如图1所示。
more >>作者:muggle
想要了解jvm,那对其内存分配管理的学习是必不可少的;java虚拟机在执行java程序的时候会把它所管理的内存划分成若干数据区域。这些区域有着不同的功能、用途、创建/销毁时间。java虚拟机所分配管理的内存区域如图1所示
程序计数器是一块比较小的内存空间,它可以看做是当前线程所执行的字节码的执行位置的指针。在虚拟机中字节码,解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的指令;虚拟机完成分支、循环、跳转、异常处理、线程恢复等功能都需要依靠它。
我们知道jvm多线程是通过线程的轮流切换并分配处理器执行时间的的方式来实现的,在任何时刻,一个处理器都只会执行一条线程中的指令。为了使线程被切换后能恢复到正确的执行位置,每条线程的程序计数器都应该是独立的,各条线程之间的计数器互不干涉,独立存储————程序计数器的内存区域为线程私有的内存。
如果线程正在执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,这个计数器的值则为空。此内存区域是唯一一个在jvm规范中没有规定任何OutOfMemoryerror情况的区域
作者:muggle
cas(比较替换):无锁策略的一种实现方式,过程为获取到变量旧值(每个线程都有一份变量值的副本),和变量目前的新值做比较,如果一样证明变量没被其他线程修改过,这个线程就可以更新这个变量,否则不能更新;通俗的说就是通过不加锁的方式来修改共享资源并同时保证安全性。
使用cas的话对于属性变量不能再用传统的int ,long等;要使用原子类代替原先的数据类型操作,比如AtomicBoolean,AtomicInteger,AtomicInteger等。
以下分类是从多个同角度来划分,而不是以某一标准来划分,请注意
作者:muggle
同步和异步通常来形容一次方法的调用。同步方法一旦开始,调用者必须等到方法结束才能执行后续动作;异步方法则是在调用该方法后不必等到该方法执行完就能执行后面的代码,该方法会在另一个线程异步执行,异步方法总是伴随着回调,通过回调来获得异步方法的执行结果;
很多人都将并发与并行混淆在一起,它们虽然都可以表示两个或者多个任务一起执行,但执行过程上是有区别的。并发是多个任务交替执行,多任务之间还是串行的;而并行是多个任务同时执行,和并发有本质区别。
对计算机而言,如果系统内只有一个cpu,而使用多进程或者多线程执行任务,那么这种情况下多线程或者多进程就是并行执行,并行只可能出现在多核系统中。当然,对java程序而言,我们不必去关心程序是并行还是并发。
作者:muggle
在安装hexo之前请确保安装了git 和node.js
打开cmd,输入
1 | npm install -g hexo-cli |
创建一个名为blog的博客项目
1 | $ hexo init blog |
新建完成后,指定文件夹的目录如下:
1 | . |
可以在 _config.yml
中修改大部分的配置。配置参数说明:
参数 | 描述 |
---|---|
title |
网站标题 |
subtitle |
网站副标题 |
description |
网站描述 |
author |
您的名字 |
language |
网站使用的语言 |
timezone |
网站时区。Hexo 默认使用您电脑的时区。时区列表。比如说:America/New_York , Japan , 和 UTC 。 |
在项目文件夹下打开cmd 执行
1 | hexo new "test" |
test 为你的文件名,在source文件下的_posts 文件夹里有一个test.md;用markdown编辑器打开,就能写博客辣。
在这个md文件头部有一个title
的属性,就是你的博客名,还可以在该头部配置日期等属性,这个文件最上方以 ---
分隔的区域叫Front-matter。以下是一些预先定义的参数,您可在模板中使用这些参数值并加以利用。
参数 | 描述 | 默认值 |
---|---|---|
layout |
布局 | |
title |
标题 | |
date |
建立日期 | 文件建立日期 |
updated |
更新日期 | 文件更新日期 |
comments |
开启文章的评论功能 | true |
tags |
标签(不适用于分页) | |
categories |
分类(不适用于分页) | |
permalink |
覆盖文章网址 |
写完文章保存后,在原来打开的命令窗口 运行
1 | hexo g |
这个指令是构建静态页面,它会在项目下生成一个public文件夹,里面就是我们hexo g
得到博客静态页面,运行
1 | hexo s |
将代码部署到本地,访问http://localhost:4000/可以查看你的博客效果
博客上传之前需要在github上建立一个仓库,仓库名称要为用户名.github.io
,因为我们博客就算基于gitpages来搭建的,所以我们要按照github的要求来命名。
创建成功之后,修改 hexo 的 _config.yml
文件,配置 GitHub 地址,如下:
1 | deploy: |
配置完成运行
1 | hexo d |
完成部署,这个时候可以访问 用户名.github.io
这个网址来查看自己的博客
1 | $ hexo init [folder] |
新建一个网站。如果没有设置 folder
,Hexo 默认在目前的文件夹建立网站。
new
1 | $ hexo new [layout] <title> |
新建一篇文章。如果没有设置 layout
的话,默认使用 _config.yml 中的 default_layout
参数代替。如果标题包含空格的话,请使用引号括起来。
1 | $ hexo new "post title with whitespace" |
generate
1 | $ hexo generate |
生成静态文件。
选项 | 描述 |
---|---|
-d , --deploy |
文件生成后立即部署网站 |
-w , --watch |
监视文件变动 |
该命令可以简写为
1 | $ hexo g |
publish
1 | $ hexo publish [layout] <filename> |
发表草稿。
server
1 | $ hexo server |
启动服务器。默认情况下,访问网址为: http://localhost:4000/
。
选项 | 描述 |
---|---|
-p , --port |
重设端口 |
-s , --static |
只使用静态文件 |
-l , --log |
启动日记记录,使用覆盖记录格式 |
deploy
1 | $ hexo deploy |
部署网站。
参数 | 描述 |
---|---|
-g , --generate |
部署之前预先生成静态文件 |
该命令可以简写为:
1 | $ hexo d |
render
1 | $ hexo render <file1> [file2] ... |
渲染文件。
参数 | 描述 |
---|---|
-o , --output |
设置输出路径 |
migrate
1 | $ hexo migrate <type> |
从其他博客系统 迁移内容。
clean
1 | $ hexo clean |
清除缓存文件 (db.json
) 和已生成的静态文件 (public
)。
在某些情况(尤其是更换主题后),如果发现您对站点的更改无论如何也不生效,您可能需要运行该命令。
list
1 | $ hexo list <type> |
列出网站资料。
version
1 | $ hexo version |
显示 Hexo 版本。
选项
安全模式
1 | $ hexo --safe |
在安全模式下,不会载入插件和脚本。当您在安装新插件遭遇问题时,可以尝试以安全模式重新执行。
调试模式
1 | $ hexo --debug |
在终端中显示调试信息并记录到 debug.log
。当您碰到问题时,可以尝试用调试模式重新执行一次,并 提交调试信息到 GitHub。
简洁模式
1 | $ hexo --silent |
隐藏终端信息。
自定义配置文件的路径
1 | $ hexo --config custom.yml |
自定义配置文件的路径,执行后将不再使用 _config.yml
。
显示草稿
1 | $ hexo --draft |
显示 source/_drafts
文件夹中的草稿文章。
自定义 CWD
1 | $ hexo --cwd /path/to/cwd |
自定义当前工作目录(Current working directory)的路径。
下载主题到./themes
目录下,修改 hexo 的 _config.yml 文件的theme属性为你的主题名就ok了,下面推荐几个hexo主题
首先申请一个域名,然后在博客所在目录下的 source 目录中,创建一个 CNAME 文件,文件内容就是你的域名,然后执行 hexo d
命令将这个文件上传到 GitHub就可以了;域名换好后需要配置域名解析。
1 | # 这是一级标题 |
百度Typora 下载安装好之后点 文件>偏好设置>勾选自动保存,这样就不怕忘记保存而文档丢失了;
快捷键:
左下角有一个 O 和 </>的符号 O表示打开侧边栏 </>查看文档源代码;
可去官网下载好主题之后点 文件>偏好设置>打开主题文件夹将解压好的主题相关文件复制粘贴到该目录下(一般是一个 主题名称文件夹 和一个 主题名称.css文件)之后重启编辑器 然后点 主题可看见安装好的主题。
作者:muggle
由于第一版排版实在太过糟糕,而且很多细节没交代清楚,所以决定写第二版;这一版争取将排版设计得清晰明了一点,以方便读者阅读。
springSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明
WebAsyncManagerIntegrationFilter:将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager 进行集成。
SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除
例如在Session中维护一个用户的安全信息就是这个过滤器处理的。HeaderWriterFilter:用于将头信息加入响应中
CsrfFilter:用于处理跨站请求伪造
LogoutFilter:用于处理退出登录
UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
BasicAuthenticationFilter:检测和处理http basic认证
RequestCacheAwareFilter:用来处理请求的缓存
SecurityContextHolderAwareRequestFilter:主要是包装请求对象request
AnonymousAuthenticationFilter:检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication
SessionManagementFilter:管理session的过滤器
ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常
FilterSecurityInterceptor:可以看做过滤器链的出口
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从cookie里找出用户的信息, 如果Spring Security能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
上一版是通过debug的方法告诉读者springSecurity的一个执行过程,发现反而把问题搞复杂了,这一版我决定画一个流程图来说明其执行过程,只要把springSecurity的执行过程弄明白了,这个框架就会变得很简单
more >>作者:muggle
markdown语法学习成本低,而且非常方便排版,如果你经常写文章,那么你就很有必要掌握markdown了,而且在vscode中编写markdown也非常方便,只需掌握几个快捷键,安装几个插件就能极大的提高你的写作效率。
ctr+shift+x 输入markdown preview enhanced下载安装,
安装好之后新建 .md文件就能愉快的写文章了,这里对markdown语法就不做介绍了,比较简单;
说一下插件怎么使用
ctrl+shift+v 打开预览,按F1或者ctr+shift+p输入Markdown Preview Enhanced: Customize Css 可改预览样式;对于一些常用的代码段还可以在vsocde中设置代码段快捷键,一键生成代码或文字。
作者:muggle
文章参考理解OAuth 2.0
在oauth2中分为以下几个角色:Resource server、Authorization server、Resource Owner、application。
Resource server:资源服务器
Authorization server:认证服务器
Resource Owner:资源拥有者
application:第三方应用
oauth2的一次请求流程为:
第三方应用获取认证token 向认证服务器认证,认证服务器通过认证后第三方应用便可向资源服务器拿资源;第三方应用的获取资源的范围由资源拥有者授权。
显然由流程可知,oauth2要保证以下几点:
1.token的有效期和更新方式要可控,安全性要好
2.用户授权第三方应用要可以控制授权范围
作者:muggle
springsecurity是一个典型的责任链模式;我们先新建一个springboot项目,进行最基本的springsecurity配置,然后debug;我这里使用的开发工具是idea.建议大家也使用idea来进行日常开发。好了话不多说,开始:
第一步
新建springboot项目 maven依赖:
1 | <dependencies> |
启动项目,控制台上会输出这样一段字符串:
1 | 2019-04-11 09:47:40.388 INFO 16716 --- [ main] .s.s.UserDetailsServiceAutoConfiguration : |
作者:muggle
netty框架代码很猛(读源码有益身心健康),学习起来也比较难;在阅读这篇文章我假设你有了一定nio基础,tcp网络协议基础,否则不建议阅读。
关于netty的学习视频我推荐B站张龙的教学视频,讲的很不错。学netty之前先学会用,然后在去看他的原理这样学起来会轻松不少。
more >>Logback是由log4j创始人设计的另一个开源日志组件,分为三个模块:
logback-core:其它两个模块的基础模块
logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API使你可以很方便地更换成其它日志系统如log4j或JDK14 Logging
logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能
在springboot中我们通过xml配置来操作logback
1 | /* |
在数学里,幂等有两种主要的定义:在某二元运算下,幂等元素是指被自己重复运算(或对于函数是为复合)的结果等于它自己的元素。如,乘法运算下,0和1符合的自乘运算符和幂等,即s*s=s某一元运算为幂等的时,其作用在任一元素两次后会和其作用一次的结果相同。例如,高斯符号便是幂等的,即f(f(x))=f(x)在计算机中,表示对同一个过程应用相同的参数多次和应用一次产生的效果是一样,这样的过程即被称为满足幂等性在分布式和前后端分离的的项目中,对于restful风格的接口,我们需要保证其接口的幂等性,说白了就是就是一个接口被反复调用不会影响最终结果;为什么呢,因为前后端分离的项目可能会发生这样的场景:前端发出一个请求,但这个请求被阻塞了,然后其重试机制再次发起请求,而恰好此时被阻塞的那个请求又好了,那么这个时候,会对后端发起连续两次请求;对于 get,put,delete 都没问题,连续的两次或者三次都不会影响请求处理结果,但post就有问题了;它会往数据库插入两条数据。
more >>邮箱:1977339740@qq.com isocket@outlook.com(常用)
微信: b3duZXJhbmRzZWxm(base64解码后便是)
职业:java程序猿
笔名: muggle
more >>tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true