一、缓存的原子性
单条命令是原子性,这是由 redis 单线程保障的,多条命令能否用multi + exec来保证其原子性呢?
对 Redis中multi + exec的认识
1.multi + exec并不支持回滚,例如
set a 1000,set b 1000 set c a
multi,decra,incrb,incrc,exec
multi相当于开始事务,他不会立刻执行命令,而且将其加入队列,当你执行exec才会执行,我们看到第三条命令,c的值并不是一个值,是一个字符串,命令加入队列时并不会去检查。

2.multi + exec中的读操作没有意义
get命令没有意思,redis事务不存在临时变量说法

3.既然multi + exec中读没有意义,就无法保证读+写的原子性,例如
set a 1000,set b 1000
get a,get b(分别为1000),现在转账500
multi,set a 500,set b 1500,exec
但如果在 get 与multi之间其它客户端修改了a或b,会造成丢失更新
在红线之前另一个客户端执行incr a,后面发现被覆盖了

![]()
二、乐观锁保证原子性
watch命令,用来盯住key(一到多个),如果这些key在事务期间:
没有被别的客户端修改,则 exec 才会成功
被别的客户端改了,则 exec 返回nil
例如
set a 1000,set b 1000
watch a b
multi
set a 500,set b 1500
exec
![]()
三、用lua 脚本保证原子性
redis 支持lua 脚本,能保证lua 脚本执行的原子性,可以取代multi + exec

四、LRU Cache(基于自写链表实现)
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);
}
}
1.如何断开节点链接

public void unlink(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}2.如何链入头节点

public void toHead(Node node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
}3、删除逻辑
①从map删除
②断开节点链接
public void remove(String key) {
Node old = this.map.remove(key);
unlink(old);
}
4、查询逻辑
①从map获取
②断开节点链接
③链入头节点
public Object get(String key) {
Node node = this.map.get(key);
if (node == null) {
return null;
}
unlink(node);
toHead(node);
return node.value;
}
5、新增逻辑
①从map获取,没有新建节点,存入map
②有则更新节点值,断开链接
③①、② 完成后均链入头节点
④检查是否超过限额,是则删除最后一个节点,并断开它的链接
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);
}
}五、LRU(LinkedHashMap实现)
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);
}
}
六、LRU Cache
Least Recently Used,将最近最少使用的key从缓存中淘汰掉
时间上,新的留下,老的淘汰
如果访问了某个 key,则它就变成最新的
实现策略:
①链表法,最近访问的key移动到链表头,不常访问的自然靠近链表尾,如果超过容量、个数限制,移除尾部的

放入1
get 43

②随机取样法,链表法占用内存较多,redis 使用的是随机取样法,每次只抽5个key,每个key记录了它们的最近访问时间,在这5个里挑出最老的移除
160已经满容量,添加一个a进去。先从其中挑五个值,36时间最早。

36会被淘汰