1. 原型模式

原型模式主要用于对象的复制,实现一个接口(实现 Cloneable 接口), 重写一个方法(重写 Object 类中的 clone 方法),即完成了原型模式。 原型模式中的拷贝分为”浅拷贝”和”深拷贝”。

浅拷贝: 对值类型的成员变量进行值的复制,对引用类型的成员变量只复制引用,不复制引用的对象。

深拷贝: 对值类型的成员变量进行值的复制,对引用类型的成员变量也进行引用对象的复制。

1.1 浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.epoint.common.util;

import java.util.ArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 浅拷贝-原型模式
*
* @author zsbo
* @version v1.0
* @date 2021/9/2 08:47
*/
public class ISimplePrototype implements Cloneable
{
private static final Logger log = LoggerFactory.getLogger(IDeepPrototype.class);

public ArrayList<String> list = new ArrayList<String>();

@Override
protected ISimplePrototype clone() {
ISimplePrototype clone = null;
try {
clone = (ISimplePrototype) super.clone();
}
catch (CloneNotSupportedException e) {
log.error(e.getMessage(), e);
}
return clone;
}

public ArrayList<String> getList() {
return list;
}

public void setList(ArrayList<String> list) {
this.list = list;
}

public void display() {
for (String str:list) {
System.out.print(str+",");
}
System.out.println();
}
}

1.2 深拷贝

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
package com.epoint.common.util;

import java.util.ArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 深拷贝-原型模式
*
* @author zsbo
* @version v1.0
* @date 2021/9/2 09:08
*/
public class IDeepPrototype implements Cloneable
{
private static final Logger log = LoggerFactory.getLogger(IDeepPrototype.class);

public ArrayList<String> list = new ArrayList<String>();

@Override
protected IDeepPrototype clone() {
IDeepPrototype clone = null;
try {
clone = (IDeepPrototype) super.clone();
clone.list = (ArrayList<String>) this.list.clone();
}
catch (CloneNotSupportedException e) {
log.error(e.getMessage(), e);
}
return clone;
}

public ArrayList<String> getList() {
return list;
}

public void setList(ArrayList<String> list) {
this.list = list;
}

public void display() {
for (String str:list) {
System.out.print(str+",");
}
System.out.println();
}
}

1.3 测试类

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
package com.epoint.common.util;

/**
* 原型模式-测试类
*
* @author zsbo
* @version v1.0
* @date 2021/9/2 08:49
*/
public class Main
{
public static void main(String[] args) {
// 浅拷贝
ISimplePrototype iSimplePrototype = new ISimplePrototype();
ISimplePrototype simpleClone = iSimplePrototype.clone();
iSimplePrototype.getList().add("tmp1");
iSimplePrototype.display();// tmp1,
simpleClone.getList().add("tmp2");
iSimplePrototype.display();// tmp1,tmp2,
simpleClone.display();// tmp1,tmp2,
System.out.println(iSimplePrototype.getList() == simpleClone.getList());// true

System.out.println("------------------------------------");

// 深拷贝
IDeepPrototype iDeepPrototype = new IDeepPrototype();
IDeepPrototype deepClone = iDeepPrototype.clone();
iDeepPrototype.getList().add("tmp1");
iDeepPrototype.display();// tmp1,
deepClone.getList().add("tmp2");
iDeepPrototype.display();// tmp1,
deepClone.display();// tmp2,
System.out.println(iDeepPrototype.getList() == deepClone.getList());// false
}
}

1.4 和单例模式关系

这两种设计模式都是处理对象创建的设计模式,区别在于:

原型模式是在已指定对象的基础上,然后通过拷贝这些原型对象创建新的对象;而单例模式模式的核心是将类的构造方法私有化,之后在类的内部产生实例化对象,并通过静态方法返回实例化对象的应用。

2. Redis

Redis是一个Key-Value的内存数据库(NoSQL),同时具备持久化的能力。同时,Redis提供面向多种语言的API,并且诸如Spring这样的框架已经给予Redis很好的支持,我们常用(Java)的Redis Client工具是Jedis。

2.1 使用场景

2.1.1 计数器

可以对 String 进行自增自减运算,从而实现计数器功能。

Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

2.1.2 缓存

将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

2.1.3 会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。

当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

2.1.4 全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。

以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

2.1.5 查找表

例如 DNS 记录就很适合使用 Redis 进行存储。

查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

2.1.6 消息队列(发布/订阅功能)

List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息

不过最好使用 Kafka、RabbitMQ 等消息中间件。

2.1.7 分布式锁实现

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。

可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

2.1.8 其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。

ZSet 可以实现有序性操作,从而实现排行榜等功能。

2.2 java客户端

2.2.1 Redis请求通信协议

Redis客户端与服务端通信使用 RESP(REdis Serialization Protocol)协议

它是一个序列化协议,支持如下几种数据类型,具体类型判断通过第一个字节判断,之间通过”\r\n”来分隔

  • 简单字符串 以”+” 开头
  • 错误类型 以”-“ 开头
  • 整数 以”:” 开头
  • 块字符串 以”$” 开头
  • 数组 以”*” 开头

客户端每次发送一个块字符串数组到服务端,服务端根据命令执行后返回结果

简单字符串

以”+”字符开头,后面接实际字符串,最后以”\r\n”结尾

因为字符是通过’\r\n’来判断结尾的,所以此种类型中的字符串内容就不能包含这特殊字符,如果有需要可以使用块字符串类型

例子:+OK\r\n

错误类型

以”-“字符开头,后面接着错误错误信息,最后以”\r\n”结尾

例子:-Error message\r\n

整数

以”:”字符开头,数值,,最后以”\r\n”结尾

例子::1000\r\n

块字符串

以”$”字符开头,后面是字符串的实际长度,之后以”\r\n”分隔,接着是字符串内容,最后以’\r\n’结尾

例子:空字符串:$0\r\n\r\n Null(不存在的值):$-1\r\n

数组

以”*”开头,后面是数组长度,之后以”\r\n”分隔,后面是具体的其他的数据值(数据类型不要求一致)

例子:空数组:*0\r\n 队列阻塞超时:*-1\r\n

2.2.2 使用过程中的常见问题及解决

无法从连接池获取到Jedis连接

JedisPool默认的maxTotal值为8,下面代码从JedisPool中获取了8个Jedis资源,但是没有归还资源。因此,当第9次尝试获取Jedis资源的时候,则无法调用jedisPool.getResource().ping()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
//向JedisPool借用8次连接,但是没有执行归还操作。
for (int i = 0; i < 8; i++) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.ping();
}
catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
jedisPool.getResource().ping();

正确示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具体的命令
jedis.executeCommand()
}
catch (Exception e) {
//如果命令有key最好把key也在错误日志打印出来,对于集群版来说通过key可以帮助定位到具体节点。
logger.error(e.getMessage(), e);
}
finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null) {
jedis.close();
}
}

具体还有一些其他原因导致无法获得连接,例如:业务并发量大,而maxTotal值设置得过小;Jedis连接被拒绝,一般是由于Redis的域名配置或网络问题等原因导致。

2.3 redis速度快的原因

纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快;

单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

采用了非阻塞I/O多路复用机制,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作,从而提高效率;

灵活多样的数据结构,redis内部使用一个redisObject对象来表示所有的key和value。redisObject主要的信息包括数据类型、编码方式、数据指针、虚拟内存等。它包含String,Hash,List,Set,Sorted Set五种数据类型,针对不同的场景使用对应的数据类型,减少内存使用的同时,节省网络流量传输。

2.4 持久化实现

Redis持久化有两种实现方式:

RDB(指定的时间间隔内保存数据快照)

全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。我们可以通过执行savebgsave命令让Redis在本地生成RDB快照文件,这个RDB文件包含了整个实例接近完整的数据内容。

AOF(先把命令追加到操作日志的尾部,保存所有的历史操作)

全称为Append Only File(追加日志文件)。它与RDB不同的是,AOF中记录的是每一个命令的详细信息,包括完整的命令类型、参数等。只要产生写命令,就会实时写入到AOF文件中。

# RDB AOF
持久化方式 生成某一时刻的数据快照文件 实时记录每一个写命令到文件
数据完整性 不完整,取决于备份周期 相对完整性高,取决于文件刷盘方式
文件大小 压缩二进制写入,文件较小 原始的操作命令,文件大
宕机恢复时间
恢复优先级
持久化代价 高,消耗大量CPU和内存 低,只占用磁盘IO资源
使用场景 数据备份、主从全量复制、对丢数据不敏感的业务场景快速数据恢复 对于丢失数据敏感的场景,例如涉及金钱交易相关的业务

2.5 高可用实现

Redis高可用的几种常见方式如下:

Redis多副本(主从复制)

Redis多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。

Redis Sentinel(哨兵)

Redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群。

其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个。

Redis Cluster(Redis集群)

Redis Cluster是社区版推出的Redis分布式集群解决方案,主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。

Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。