06_2、【Java面试-缓存篇】(下)

120 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

6. 缓存原子性

  • 掌握 Redis 事务的局限性

  • 理解用乐观锁保证原子性

  • 理解用 lua 脚本保证原子性

Redis 事务局限性

  • 单条命令是原子性,这是由 redis 单线程保障的

  • 多条命令能否用 multi + exec 来保证其原子性呢?

Redis 中 multi + exec 并不支持回滚,例如有初始数据如下


set a 1000

set b 1000

set c a

执行


multi

decr a

incr b

incr c

exec

执行 incr c 时,由于字符串不支持自增导致此条命令失败,但之前的两条命令并不会回滚

更为重要的是,multi + exec 中的读操作没有意义,因为读的结果并不能赋值给临时变量,用于后续的写操作,既然 multi + exec 中读没有意义,就无法保证读 + 写的原子性,例如有初始数据如下


set a 1000

set b 1000

假设 a 和 b 代表的是两个账户余额,现在获取旧值,执行转账 500 的操作:


get a /* 存入客户端临时变量 */

get b /* 存入客户端临时变量 */

/* 客户端计算出 a 和 b 更新后的值 */

multi

set a 500

set b 1500

exec

但如果在 get 与 multi 之间其它客户端修改了 a 或 b,会造成丢失更新

乐观锁保证原子性

watch 命令,用来盯住 key(一到多个),如果这些 key 在事务期间:

  • 没有被别的客户端修改,则 exec 才会成功

  • 被别的客户端改了,则 exec 返回 nil

还是上一个例子


get a /* 存入客户端临时变量 */

get b /* 存入客户端临时变量 */

/* 客户端计算出 a 和 b 更新后的值 */

watch a b /* 盯住 a 和 b */

multi

set a 500

set b 1500

exec

此时,如果其他客户端修改了 a 和 b 的值,那么 exec 就会返回 nil,并不会执行两条 set 命令,此时客户端可以进行重试

lua 脚本保证原子性

Redis 支持 lua 脚本,能保证 lua 脚本执行的原子性,可以取代 multi + exec

例如要解决上面的问题,可以执行如下命令


eval "local a = tonumber(redis.call('GET',KEYS[1]));local b = tonumber(redis.call('GET',KEYS[2]));local c = tonumber(ARGV[1]); if(a >= c) then redis.call('SET', KEYS[1], a-c); redis.call('SET', KEYS[2], b+c); return 1;else return 0; end" 2 a b 500

  • eval 用来执行 lua 脚本

  • 2 表示后面用空格分隔的参数中,前两个是 key,剩下的是普通参数

  • 脚本中可以用 keys[n] 来引用第 n 个 key,用 argv[n] 来引用第 n 个普通参数

  • 其中双引号内部的即为 lua 脚本,格式化如下


local a = tonumber(redis.call('GET',KEYS[1]));

local b = tonumber(redis.call('GET',KEYS[2]));

local c = tonumber(ARGV[1]); 

if(a >= c) then 

    redis.call('SET', KEYS[1], a-c); 

    redis.call('SET', KEYS[2], b+c); 

    return 1;

else 

    return 0end

7. LRU Cache 实现

要求

  • 掌握基于链表的 LRU Cache 实现

  • 了解 Redis 在 LRU Cache 实现上的变化

LRU Cache 淘汰规则

Least Recently Used,将最近最少使用的 key 从缓存中淘汰掉。以链表法为例,最近访问的 key 移动到链表头,不常访问的自然靠近链表尾,如果超过容量、个数限制,移除尾部的

  • 例如有原始数据如下,容量规定为 3

image-20210902141534470.png

  • 时间上,新的留下,老的淘汰,比如 put d,那么最老的 a 被淘汰

image-20210902141720278.png

  • 如果访问了某个 key,则它就变成最新的,例如 get b,则 b 被移动到链表头

image-20210902141912068.png

  

LRU Cache 链表实现

  • 如何断开节点链接

image-20210902141247148.png

  • 如何链入头节点

image-20210902141320849.png

参考代码一


package day06;



import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;



public class LruCache1 {

    static class Node {

        Node prev;

        Node next;

        String key;

        Object value;

\


        public Node(String key, Object value) {

            this.key = key;

            this.value = value;

        }




        // (prev <- node -> next)

        public String toString() {

            StringBuilder sb = new StringBuilder(128);

            sb.append("(");

            sb.append(this.prev == null ? null : this.prev.key);

            sb.append("<-");

            sb.append(this.key);

            sb.append("->");

            sb.append(this.next == null ? null : this.next.key);

            sb.append(")");

            return sb.toString();

        }

    }




    public void unlink(Node node) {

        node.prev.next = node.next;

        node.next.prev = node.prev;

    }




    public void toHead(Node node) {

        node.prev = this.head;

        node.next = this.head.next;

        this.head.next.prev = node;

        this.head.next = node;

    }



    int limit;

    Node head;

    Node tail;

    Map<String, Node> map;

    public LruCache1(int limit) {

        this.limit = Math.max(limit, 2);

        this.head = new Node("Head", null);

        this.tail = new Node("Tail", null);

        head.next = tail;

        tail.prev = head;

        this.map = new HashMap<>();

    }


    public void remove(String key) {

        Node old = this.map.remove(key);

        unlink(old);

    }




    public Object get(String key) {

        Node node = this.map.get(key);

        if (node == null) {

            return null;

        }

        unlink(node);

        toHead(node);

        return node.value;

    }




    public void put(String key, Object value) {

        Node node = this.map.get(key);

        if (node == null) {

            node = new Node(key, value);

            this.map.put(key, node);

        } else {

            node.value = value;

            unlink(node);

        }

        toHead(node);

        if(map.size() > limit) {

            Node last = this.tail.prev;

            this.map.remove(last.key);

            unlink(last);

        }

    }




    @Override

    public String toString() {

        StringBuilder sb = new StringBuilder();

        sb.append(this.head);

        Node node = this.head;

        while ((node = node.next) != null) {

            sb.append(node);

        }

        return sb.toString();

    }




    public static void main(String[] args) {

        LruCache1 cache = new LruCache1(5);

        System.out.println(cache);

        cache.put("1", 1);

        System.out.println(cache);

        cache.put("2", 1);

        System.out.println(cache);

        cache.put("3", 1);

        System.out.println(cache);

        cache.put("4", 1);

        System.out.println(cache);

        cache.put("5", 1);

        System.out.println(cache);

        cache.put("6", 1);

        System.out.println(cache);

        cache.get("2");

        System.out.println(cache);

        cache.put("7", 1);

        System.out.println(cache);

    }




}

参考代码二


package day06;




import java.util.LinkedHashMap;

import java.util.Map;




public class LruCache2 extends LinkedHashMap<String, Object> {




    private int limit;



    public LruCache2(int limit) {

        // 1 2 3 4 false

        // 1 3 4 2 true

        super(limit * 4 /3, 0.75f, true);

        this.limit = limit;

    }




    @Override

    protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {

        if (this.size() > this.limit) {

            return true;

        }

        return false;

    }




    public static void main(String[] args) {

        LruCache2 cache = new LruCache2(5);

        System.out.println(cache);

        cache.put("1", 1);

        System.out.println(cache);

        cache.put("2", 1);

        System.out.println(cache);

        cache.put("3", 1);

        System.out.println(cache);

        cache.put("4", 1);

        System.out.println(cache);

        cache.put("5", 1);

        System.out.println(cache);

        cache.put("6", 1);

        System.out.println(cache);

        cache.get("2");

        System.out.println(cache);

        cache.put("7", 1);

        System.out.println(cache);

    }

}

Redis LRU Cache 实现

Redis 采用了随机取样法,较之链表法占用内存更少,每次只抽 5 个 key,每个 key 记录了它们的最近访问时间,在这 5 个里挑出最老的移除

  • 例如有原始数据如下,容量规定为 160,put 新 key a

image-20210902142353705.png

  • 每个 key 记录了放入 LRU 时的时间,随机挑到的 5 个 key(16,78,90,133,156),会挑时间最老的移除(16)

  • 再 put b 时,会使用上轮剩下的 4 个(78,90,133,156),外加一个随机的 key(125),这里面挑最老的(78)

image-20210902142644518.png

  • 如果 get 了某个 key,它的访问时间会被更新(下图中 90)这样就避免了它下一轮被移除

image-20210902143122234.png