0xCAFEBABE

Just for fun


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于

  • 搜索

Spring AOP的两种实现:JDK/CGLib动态代理

发表于 2019-02-26 | 分类于 Java | | 阅读次数:
字数统计: 3,016 字

前言

AOP 代理主要分为静态代理和动态代理,静态代理的代表为 AspectJ,而动态代理则以 Spring AOP 为代表。静态代理是编译期实现,动态代理是运行期实现,静态代理拥有相对更好的性能。

静态代理在编译阶段生成 AOP 代理类,生成的字节码就织入了增强后的 AOP 对象。动态代理则不会修改字节码,而是在内存中临时生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP 中的动态代理

Spring AOP 中存在两种代理实现机制,分别是 JDK 动态代理和 CGLib 动态代理。

Spring 中虽然使用了与 AspectJ 一样的注解,没有使用 AspectJ 的编译器 ,采用的仍是 Spring AOP 动态代理,而是 AspectJ 静态代理。

Spring 在选择用 JDK 还是 CGLib 的依据:

  1. 当 Bean 实现接口时,Spring 就会用 JDK 的动态代理。
  2. 当 Bean 没有实现接口时则使用 CGlib实现。

注:从 Spring Boot 2.0 开始,默认的 AOP 策略已经更换为 CGLIB,即 proxy-target-class = true。

JDK 动态代理

JDK 动态代理使用步骤

  1. 定义 InvocationHandler 类并实现 InvocationHandler 接口。

  2. 通过 Proxy.newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h) 方法生成动态代理类实例。

  3. 通过生成的动态代理类实例调用目标方法,目标方法的调用会被转发给 InvocationHandler 类中的 invoke() 方法处理。

JDK 动态代理源码分析

JDK 动态代理通过反射创建 Class 并动态生成类字节码加载到 JVM 中,并且要求被代理的类必须实现一个接口,核心是 InvocationHandler 接口和 Proxy 类。

1
2
3
4
5
6
7
public interface InvocationHandler {
// InvocationHandler is the interface implemented by the invocation handler of a proxy instance
// InvocationHandler接口由proxy实例的invocation handler实现
// 当通过proxy实例调用一个方法时候,这个方法的调用就会被转发给实现了InvocationHandler接口的invocation handler中的invoke方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

Proxy 类则是用来创建一个代理对象的类,先来看看生成代理对象的 newProxyInstance() 方法,这个方法接收接受以下三个参数:

  • ClassLoader loader:定义由哪个 Classloader 对象负责加载生成的代理对象。
  • Class<?>[] interfacess:接口数组,表示提供给代理对象的一组接口,代理类可以调用接口数组中实现的所有方法。
  • InvocationHandler h:表面当代理对象调用方法的时候会关联到哪一个 InvocationHandler 对象上,并最终由其调用。

通过 Proxy.newProxyInstance() 方法创建的代理对象是在 JVM 运行时动态生成的一个对象。

阅读全文 »

Raft算法与Gossip协议原理

发表于 2019-02-22 | 分类于 分布式系统 | | 阅读次数:
字数统计: 3,308 字

前言

Consul 中使用 Raft 算法解决一致性问题,使用 Gossip 协议来管理集群成员和广播消息,其中 Raft 属于强一致性模型,Gossip 属于弱一致性模型。不仅是 Consul,Redis Cluster 中也使用了 Gossip 协议。

Raft 算法

Raft 算法中的 Server 有以下三种状态:Leader(领导者)、Follower(跟随者)和 Candidate(候选者)。

Leader 负责处理所有的查询和事务,并向 follower 同步事务。Follower 会将所有的 RPC 查询和事务转发给 leader 处理,所有 follower 仅从 leader 接受数据同步,数据的一致性以 leader 中的数据为准实现。

1

Raft 算法将一致性问题分解成为三个相对独立的子问题:

  • 领导选取(Leader election):在一个 leader 宕机之后必须要选取一个新的leader。

  • 日志复制(Log replication):Leader 必须从 client 接收 log 然后复制到集群中的其他 follower,并且强制要求其他 follower 的 log 保持和自己相同。

  • 安全性(Safety):Raft 的 Safety 指的是 State Machine Safety(状态机安全),其含义是如果一个 server 已经将给定索引位置的 log 应用到状态机中,则所有其他 server 不会在该索引位置应用不同的log(log 一致)。

Leader Election

情形1:选举成功

Raft 算法采用 timeout 来控制选举过程,初始化的选举过程如下:

  1. 在集群初始化启动时,节点的 Raft 状态机将处于follower 状态等待来自 leader 的心跳。

  2. 如果 follower 在 timeout 内没有接收到来自 leader 的心跳,则会将自己的状态切换为 candidate,然后向其他的 follower 发起选举请求,询问其是否将自己选为 leader。每次开启的一轮新的选举被称为一个”任期”,每个任期都有一个严格递增整数值 term 与其对应。

  3. 当某个 candidate 收到来自集群中超过 quorum (通常为半数)的投票后,即成为 leader,它将发送心跳消息给其他节点来宣告它的权威性以阻止其它节点再发起新的选举,并开始接收 client 的请求。此时一轮选举结束,集群内的每个节点的 term 都会加 1。每个 follower 在一个任期中只能投出一票,而且遵守”先到先服务”的原则。

  4. 新的 leader会继续向 follower发送心跳,心跳包里包含 client 的事务处理和查询请求,每次 follower接收到来自 leader 的心跳后,就会刷新自己的 timeout。

阅读全文 »

深入理解Redis SDS原理

发表于 2019-02-21 | 分类于 Redis | | 阅读次数:
字数统计: 879 字

SDS

在 Redis 并没有直接使用 C 中的字符串,而是用一种称为 SDS(Simple Dynamic String)的结构体来保存字符串。

例如:执行命令 set key value,key 和 value 都是一个 SDS 类型的结构存储在内存中。

除了用作默认的字符串表示外,SDS 还被用作缓冲区(buffer),比如 AOF 缓冲区和客户端中的输入缓冲区。

SDS 的结构

sds

1
2
3
4
5
6
7
8
9
struct sdshdr {
// buf数组中已使用字节数
// 等于SDS中保存字符串的长度
int len;
// buf数组中未使用字节数
int free;
// 字节数组,用于保存字符串
char buf[];
};

SDS 遵循 C 字符串以"\0" 结尾的惯例,最后一个保存空字符串的 1 字节空间不计算在 len 中。

阅读全文 »

一致性哈希算法原理及Java实现

发表于 2019-02-20 | 分类于 分布式系统 | | 阅读次数:
字数统计: 1,323 字

前言

一致性哈希算法在 1997 年由 MIT 的 Karger 等人在解决分布式 Cache 中而发表的一篇 paper “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web” 中提出,设计目标是为了解决分布式缓存热点(Hot spot)问题。

这篇 paper 提出在动态变化的 Cache 环境中,哈希算法应该满足的4个适应条件:Balance(均衡)、Monotonicity(单调性)、Spread(分散性)、Load(负载)。

一致性哈希算法可应用于负载均衡、分布式缓存等场景中。以分布式缓存为例,对于 $n$ 台缓存服务器($n$ 个 node),普通哈希是对资源 $o$ 的请求使用 $ {{\mbox{hash}}(o) \ \% \ n} $ 来映射到某个 node上。这种方式强依赖于 node 的数量,所以当 node 增加或减少时就需要对大量资源进行 rehash。

使用一致性哈希算法,可以很好地解决这个问题,它能够尽可能小的改变 node 和 已存在 key 的映射关系。

一致性哈希算法

一致性哈希算法通常借助一个被称为哈希环的整数环来实现,哈希环的值空间为 $ 2^{32} $ , 即 $ 0 \sim 2^{32}-1 $,其中 Node 按顺时针方向分布于哈希环上。对于每个资源(比如 IP 地址),可以通过 $ { {\mbox{hash}}(o) \ \% \ 2^{32}} $ 来映射到哈希环上,每个资源分得处于自己前方顺时针方向的 Node。

例如下图,IP 1 分给 Node A, IP2 与 IP3 分给 Node B,以此类推。

1

阅读全文 »

Java泛型原理及总结

发表于 2019-02-18 | 分类于 Java | | 阅读次数:
字数统计: 2,799 字

前言

虽然平时除了一些集合类以外很少会用到泛型,但深入了解泛型一直是以前的 Todo list 之一,拖了挺久了,这次就借这篇笔记来一次解决。

这篇笔记参考自 Oracle Java 官方文档中的 Generics 部分与 Baeldung 的 这篇文章,示例代码也来自于Oracle Java 官方文档。

重点:通配符、PECS 原则、类型擦除。

关于泛型

“In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. The difference is that the inputs to formal parameters are values, while the inputs to type parameters are types.”

泛型(Generics)可以参数化类型,提供了一种处理不同输入下复用代码的方式。普通参数输入的是值,而泛型参数的输入是类型。简而言之,泛型就是类型参数(Type Parameters)。

那么,为何要引入泛型呢,Oracle 是这么解释的:

“Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming.”

为了编译期中提供更为严格的类型检查,泛型被引入了 Java 中。简单来说,泛型可以将错误 / 异常提前到编译期,而不是运行期,这样我们就可以在编译期解决问题,而不是等到其在运行期才暴露出来。

泛型中的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。

泛型的优点

  • Stronger type checks at compile time (编译期中更具健壮性的类型检查)
    Java 编译器会在编译期对泛型进行类型安全检查,编译报错相比运行错误更容易发现。
  • Elimination of casts (消除强转)
  • Enabling programmers to implement generic algorithms (编写更具复用性的程序)
    使用泛型,可以令程序灵活处理不同的参数类型而不用改变代码。
阅读全文 »

Docker网络模式踩坑

发表于 2019-02-15 | 分类于 Docker | | 阅读次数:
字数统计: 1,115 字

前言

这篇笔记源自上次 Docker 下搭建 Redis 哨兵集群所踩的坑,主要原因是 Docker 默认的网络模式是 Bridge,而在设置哨兵的 matser IP 时用的是容器的内网地址,在 Spring Boot 中使用 Redis 时哨兵返回的仍是 Docker 的内网地址。

解决办法是加入 docker 启动参数 --network=host,改为 host 模式启动,并将 master 节点 IP 改为 host 的外网地址,此时的 Redis 集群是直接暴露在公网的,一定要设置 requirepass。

借此机会,顺便学习一下 Docker 的网络模式。

Docker 的网络模式

Dcoker 网络隔离基于 Network namespace,创建 Docker 容器时会为每一个容器分配 Network namespace。

创建 Docker 容器时有以下几种网络模式:

  • Bridge
  • Host
  • None
  • Overlay
  • Macvlan
  • Container

Docker 容器默认为 bridge 模式,其他模式需要在创建容器时使用 –-net 手动指定。

Bridge 模式

Bridge 模式下会在 host 上创建一个 名为 docker0 的虚拟网桥,此模式下的容器会桥接到这个虚拟网桥上,虚拟网桥会与 host 进行 IP 转换,端口映射等通信。

Docker 官方文档中这样说明 Bridge 的使用场景:

“ Bridge networks are usually used when your applications run in standalone containers that need to communicate. “
Bridge 网络通常用于应用程序在需要通信的独立容器中运行时的场景下。

Host 模式

指定方法:--net="host"

Host 模式删除了容器与 host 之间的网络隔离(“remove network isolation between the container and the Docker host”),此模式下创建的容器没有自己独立的 Network namespace,而是和 host 共享一个 Network namespace 并且共享 host 的所有端口与 IP。

通过这种模式创建的容器可以访问看到 host 上所有的网络设备,由于这种方式创建的容器有极高的访问权限,所以被认为是不安全的。

阅读全文 »

深入理解Redis Master-Slaver/Sentinel/Cluster原理

发表于 2019-02-14 | 分类于 Redis | | 阅读次数:
字数统计: 5,905 字

前言

这篇总结参考自 Redis 的官方文档与《Redis设计与实现》。

Redis中的多实例架构主要分为三种:主从(Master-Slaver)、哨兵(Sentinel) 和集群(Cluster),其中 Sentinel 和 Cluster 都是以 Master-Slaver 为基础的。

主从(Master-Slaver)

注:由于某些政治正确原因,Redis 官方现在已经将 Master-Slaver 改为 Master-Replica,一些相关的命令诸如 slaverof <Matser-IP> 也改成了 replicaof <Matser-IP>,但原有的命令暂时还是可用并存的。

个人理解,主从模式的作用有以下两点:

  • 容灾备份:当一个节点损坏时由于有备份,可以十分容易地恢复数据。
  • 负载均衡:通过读写分离,所有写请求将由 master完成,读请求则可以通过其他 slave 来完成且 slave 是可以无限扩展的,从而可以支持更高的并发量。

主从模式下,一个 master 可以有多个 slave,但一个 slave 只能有一个master,slave 也可以有自己的 slave,其中的数据流向是单向的,由 master 到 slave。

注意,出于保证主和各个从之间数据一致性的考虑,slave 默认是无法提供写服务的,除非将配置文件中的 replica-read-only 改为 no,但这样的话就要自己实现主从一致性。

1
2
# Since Redis 2.6 by default replicas are read-only.
replica-read-only yes

旧版复制(SYNC)

旧版复制功能可以分为同步与命令传播两个操作:

  • 同步:用于将 slave 更新至与 master 数据一致的状态。
  • 命令传播:用于 master 数据被更改,导致主从不一致的情况,让 master 与 slave 重新回到数据一致的在状态。

同步

同步操作需要 SYNC 命令来完成,具体步骤如下:

  1. 从节点向 master 发送 PSYNC 命令。
  2. Master 执行 BGSAVE,开启一个后台进程生成 RDB 文件。
  3. Master 开始缓冲所有新的写命令。
  4. 当后台保存任务完成时, master 将 RDB 文件读取至内存中并传输给 slave, slave 将其保存在磁盘上,然后加载 RDB 文件到内存进行同步。
  5. Master 将缓冲区内缓存的所有写命令发送给 slave,slave 执行这些写命令完成全量复制。

命令传播

经过同步操作后,主从之间的数据将达到一致状态,但当 master 数据又被改变时,为了让主从再次回到数据一致状态,master 还需要对 slave 进行命令传播。

即 master 会将新的造成数据不一致的命令都发送给 slave,这样当 slave执行完这些命令后,主从将恢复数据一致状态。

阅读全文 »

基于Docker搭建Redis一主二从三哨兵集群实战

发表于 2019-02-12 | 分类于 Redis | | 阅读次数:
字数统计: 1,027 字

前言

毕设中使用了 Redis 作为缓存,自己以往的项目中使用的都是单机的 Redis,这次要将单机的 Redis 实例改为 Redis 集群。

更新:这种方式部署有坑,外网环境下无法访问该集群,解决办法已更新在《Docker网络模型踩坑》。

搭建步骤

系统环境:Centos7.4 & Docker 18.09.2

首先拉取Redis镜像。

1
$ docker pull redis

搭建一主二从集群,yourpassword 为自定义密码,注意主从密码需要一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# master
$ docker run -it --name redis-master -d -p 6379:6379 redis redis-server --requirepass yourpassword --port 6379
$ docker exec -it redis-master bash
$ redis-cli -a yourpassword -p 6379
$ config set masterauth yourpassword

# slave1
$ docker run -it --name redis-slave1 -d -p 6380:6380 redis redis-server --requirepass yourpassword --port 6380
$ docker exec -it redis-slave1 bash
$ redis-cli -a yourpassword -p 6380
$ config set masterauth yourpassword

# slave2
$ docker run -it --name redis-slave2 -d -p 6381:6381 redis redis-server --requirepass yourpassword --port 6381
$ docker exec -it redis-slave2 bash
$ redis-cli -a yourpassword -p 6381
$ config set masterauth yourpassword

下载 Redis 官方的哨兵配置 sentinel.conf。

1
$ wget http://download.redis.io/redis-stable/sentinel.conf
阅读全文 »

Java中的强引用、弱引用、软引用和虚引用

发表于 2019-02-10 | 分类于 Java | | 阅读次数:
字数统计: 779 字

前言

在看 JDK 中 ThreadLocal 源码时接触到了弱引用 WeakReference,在这次的笔记中记录下强引用、弱引用、软引用和虚引用的概念与区别。

四种引用的级别由高到低依次为:

Strong Reference > Soft Reference > Weak Reference > Phantom Reference

Reference

强引用(Strong Reference)

强引用是最常使用的引用,任何被强引用所引用的对象都不能被垃圾回收器回收。当内存空间不足,JVM 就会抛出 OutOfMemoryError 错误。

1
Object obj = new Object();

变量 obj 就是该 Object 对象的一个强引用。只有强引用消失,对象才能被 GC 回收。

如果在一个方法的内部有一个强引用,这个引用保存在 Java 栈中,而真正的引用内容(Object)保存在 Java堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。

弱引用(Weak Reference)

相比于软引用,具有弱引用的对象拥有更短暂的生命周期。当一个对象只有弱引用指向它时,垃圾回收器不管当前内存是否足够,都会进行回收。

在 ThreadLocal 中的 ThreadLocalMap 的 key 就被设计成了弱引用类型,在 JDK 中另一个使用弱引用的例子是WeakHashMap,它是除 HashMap 和 TreeMap 之外Map接口的另一种实现。WeakHashMap中的 key 也是弱引用类型。

阅读全文 »

Java8 ThreadLocal源码阅读

发表于 2019-02-07 | 分类于 Java | | 阅读次数:
字数统计: 5,507 字

ThreadLocal 原理

每个线程内部维护着一个 ThreadLocalMap,这个 Map 的底层是由一个 Entry 数组实现的。Entry 中的 key 为弱引用类型,指向 ThreadLocal 本身,value 则是实际存储的变量 Object。

ThreadLocal

概述

ThreadLocal 的作用在于提供了「线程内的局部变量」,该变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

其他线程无法访问 ThreadLocal 内的变量,这样就隔离了多个线程间的资源,也不会出现线程安全问题。

Get 策略

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
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

protected T initialValue() {
return null;
}
  1. 获取当前线程。
  2. 获取当前线程的 ThreadLocalMap。
  3. 如果 ThreadLocalMap 不为空,以 ThreadLocal 的引用作为 key 来在 ThreadLocalMap 中获取对应的value e。
  4. if (e != null && e.get() == key),返回 e。
  5. ThreadLocalMap 为 null 或 value 为 null,则调用 setInitialValue() 方法返回初始值。
阅读全文 »
123…5
Marticles

Marticles

48 日志
15 分类
15 标签
GitHub E-Mail
Creative Commons
© 2018 LJH's Blog | 累计字数 137.1k
载入天数...载入时分秒...
访问量