1 分布式缓存理论
1.1 大型网站架构
1.2 缓存概况
缓存分为:客户端缓存、网络中的缓存、服务端缓存。
1.3 幂等性
概念:1次调用和N次调用返回一样的结果。
保证措施:通常在调用时加上唯一标识,例如订单号。
1.4 分布式系统理论
1.4.1 CAP理论
一致性(C):分布式系统中所有数据备份在同一时刻拥有相同的值;
可用性(A):集群中一部分节点故障后,集群整体还能响应客户端的读写请求;
分区容忍性(P):如果系统不能在一定时限内达成数据一致性,就意味着发生了分区,必须就当前操作在C和A之间做出选择。
1.4.2 分布式一致性协议
(1)Paxos
多个节点之间就某个值(提案)达成一致(决议)的而通信协议。
分为两个阶段:Prepare阶段和Accept阶段。
参与者角色:Proposer——提一提案的服务器,Acceptor——批准提案的服务器,二者在物理上可以是同一台机器。
- Prepare阶段:Proposer发送Prepare、Acceptor应答Prepare;
- Accept阶段:Proposal发送Accept、Acceptor应答Accept。
(2)2PC
- 阶段1:提交请求阶段(投票阶段)
- 阶段2:提交阶段
不足:提交协议是阻塞型协议,如果协调器宕机则参与者无法解决事务。
(3)3PC
- 阶段1:canCommit,投票,事务协调器询问参与者是否能提交;
- 阶段2:preCommit,预提交;
- 阶段3:doCommit,提交,一般通过重试补偿策略保证doCommit提交成功。
(4)Raft
任何时候一台服务器可以扮演以下角色之一:
领导者、选民、候选人
也分为2个阶段,第一阶段选举,第二阶段正常操作
1.4.3 解决“脑裂”问题
心跳机制 + Monitor
1.4.4 负载均衡
(1)轮询:Round Robin,根据Nginx配置文件中的顺序,依次把客户端的Web请求分发到不同的后端服务器;
(2)最少连接:谁的连接最少就分发给谁
(3)IP地址哈希:来自相同IP的请求转发给后端同一台服务器处理,方便session保持
(4)基于权重的负载均衡:把请求更多的分发给配置高的服务器上。
2 手写LRU缓存
双向链表实现,不带超时策略。链表首部表示最近的数据,尾部表示最远使用的数据。head仅仅作为哨兵,不保存任何数据;tail则保存了实际数据。
(1)put(E e):插入一个元素。如果超过缓存容量,则先从链表尾部删除一个元素再插入首部,否则直接插入首部;
(2)get(E e):没有就返回null;有就返回值,并且把该元素提到链表首部。
package cache;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description LRU淘汰算法(链表实现,线程安全)
* @Author lilong
* @Date 2019-02-27 18:50
*/
public class LRUCache<E> {
// 双向链表,用于缓存数据
static class Node<E> {
E item;
Node<E> pre;
Node<E> next;
Node(E x) {
item = x;
}
}
// 缓存容量
private final int capacity;
private final AtomicInteger count = new AtomicInteger();
private Node<E> head; //头结点
private Node<E> tail; //尾节点
public LRUCache() {
this(Integer.MAX_VALUE);
}
public LRUCache(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException();
}
this.capacity = capacity;
tail = head = new Node<E>(null);
}
public synchronized void put(E e) {
// 缓存已满,淘汰尾节点
if (count.get() == capacity) {
removeLast();
}
// 然后将新节点插入到头部
insertHead(e);
}
/**
* 删除链表最后一个元素
*/
private void removeLast() {
if (count.get() == 0) {
return;
}
// 只有一个节点的删除
if (count.get() == 1) {
tail = head = new Node<>(null);
} else {
// 将末尾节点的前一个节点作为新的尾节点
tail = tail.pre;
tail.next = null;
}
count.getAndDecrement();
}
/**
* 将新元素插入链表头部
* @param e
*/
private void insertHead(E e) {
Node<E> newNode = new Node<>(e);
insertHead(newNode);
}
private void insertHead(Node<E> newNode) {
if (count.get() == 0) {
head.next = newNode;
newNode.pre = head;
tail = newNode;
} else {
Node<E> tmp = head.next;
head.next = newNode;
newNode.pre = head;
tmp.pre = newNode;
newNode.next = tmp;
}
count.getAndIncrement();
}
public synchronized E get(E e) {
if (count.get() == 0) {
return null;
}
Node<E> tmp = head;
while (tmp != null && tmp.next != null) {
tmp = tmp.next;
if (e.equals(tmp.item)) {
E e1 = tmp.item;
// 将该元素提到开头处
moveToHead(tmp);
return e1;
}
}
return null;
}
/**
* 将元素移到首位
* @param node
*/
private void moveToHead(Node<E> node) {
if (node.pre == head) {
return;
}
// 先删除该元素
if (node == tail) {
removeLast();
} else {
node.pre.next = node.next;
node.next.pre = node.pre;
}
//然后插入首部
insertHead(node);
}
// 打印缓存中的所有元素
@Override
public String toString() {
List<E> list = new ArrayList<>();
if (count.get() == 0) {
return "Empty";
}
Node<E> tmp = head;
while (tmp != null && tmp.next != null) {
list.add(tmp.next.item);
tmp = tmp.next;
}
return list.toString();
}
}
测试下:
新建一个容量为3的缓存:
package cache;
/**
* @Description 测试缓存
* @Author lilong
* @Date 2019-02-27 19:21
*/
public class Test {
public static void main(String[] args) {
LRUCache<Integer> cache = new LRUCache<>(3);
for (int i = 2 ; i < 9; i++) {
cache.put(i);
System.out.println(cache);
}
System.out.println("################");
for (int i = 0 ; i < 10; i++) {
printCacheEle(cache, i);
}
}
private static void printCacheEle(LRUCache<Integer> cache, int i) {
Integer a = cache.get(i);
System.out.println(a);
System.out.println(cache);
}
}
打印出来:
[2]
[3, 2]
[4, 3, 2]
[5, 4, 3]
[6, 5, 4]
[7, 6, 5]
[8, 7, 6]
################
null
[8, 7, 6]
null
[8, 7, 6]
null
[8, 7, 6]
null
[8, 7, 6]
null
[8, 7, 6]
null
[8, 7, 6]
6
[6, 8, 7]
7
[7, 6, 8]
8
[8, 7, 6]
null
[8, 7, 6]
3 分布式缓存应用
redis缓存原理参考我的另一篇博客:https://blog.csdn.net/u010266988/article/details/88374218
3.1 Redis、Memcached对比
1)数据类型:Redis 5种——string、list、hash、set、zset(跳跃表);
Memcache——只支持键值对;
2)线程模型:Redis——单线程,可以在单台机器部署多个实例;
Memcached——所线程(俩都是非阻塞I/O模型)
3)持久机制:Redis——RDB(定时持久)、AOF(基于操作日志)
4)高可用:Redis——支持主从节点复制配置
5)事务:Redis——支持事务
6)数据淘汰策略:Redis——丰富的淘汰策略;
Memcached——只有LRU策略
3.2 Redis集群
异步复制,数据发到主节点,然后异步复制到从节点。
Redis集群分片机制:Hash槽,总共16384个Hash槽,每个节点分摊一段。
一致性哈希:
把集群中的机器分布在0-2^32的圆上,如果要缓存一个数据,先计算数据的Key的哈希值,然后顺时针找到第一台机器。
3.3 应用层访问缓存的模式
1)双读双写:读操作先读缓存、读不到再读数据库同时回写缓存;写操作先写数据库再写缓存;
2)异步更新:应用层只读缓存、只写DB,然后用定时任务或binlog把DB数据同步到缓存;
3)串联:应用层读、写缓存,缓存再读、写DB(不推荐)
3.4 分布式缓存扩容迁移步骤
1)上双写:同时写新老缓存,新的按新分片规则,老的按老分片规则;
2)迁移历史数据:把老的数据按新规则迁移到新的分片;
3)切读:读操作从老分片切到新分片;
4)下双写:旧分片下线。
3.5 缓存穿透、缓存并发、缓存雪崩
1)缓存穿透:大量访问一个不存在的key。解决办法:把空值也缓存起来,缓存一个较短的时间;
2)缓存并发:一个缓存key过期时,因为访问量巨大导致瞬间都去请求数据库。解决办法:分布式锁、软过期;
3)缓存雪崩:大量缓存集中失效。解决办法:对过期时间加一个随机值。
4 本地缓存
guava cache的使用:https://blog.csdn.net/u010266988/article/details/88584228