持续创作,加速成长!这是我参与「掘金日新计划 · 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 0;
end
7. LRU Cache 实现
要求
-
掌握基于链表的 LRU Cache 实现
-
了解 Redis 在 LRU Cache 实现上的变化
LRU Cache 淘汰规则
Least Recently Used,将最近最少使用的 key 从缓存中淘汰掉。以链表法为例,最近访问的 key 移动到链表头,不常访问的自然靠近链表尾,如果超过容量、个数限制,移除尾部的
- 例如有原始数据如下,容量规定为 3
- 时间上,新的留下,老的淘汰,比如 put d,那么最老的 a 被淘汰
- 如果访问了某个 key,则它就变成最新的,例如 get b,则 b 被移动到链表头
LRU Cache 链表实现
- 如何断开节点链接
- 如何链入头节点
参考代码一
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
-
每个 key 记录了放入 LRU 时的时间,随机挑到的 5 个 key(16,78,90,133,156),会挑时间最老的移除(16)
-
再 put b 时,会使用上轮剩下的 4 个(78,90,133,156),外加一个随机的 key(125),这里面挑最老的(78)
- 如果 get 了某个 key,它的访问时间会被更新(下图中 90)这样就避免了它下一轮被移除