分布式缓存

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

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值