目录
-
1 缓存介绍和分类
- 1.本地缓存
-
2 缓存使用注意事项
- 2.1 数据更新先操作缓存还是数据库
- 2.2 并发更新大坑
-
3 手写LRU缓存
1 缓存介绍和分类
1.1 介绍
用户请求 -> 界面 -> 网络转发 -> 应用服务 —> 数据服务
- 应用服务器资源是有限的,且技术变革是缓慢的,数据库每秒能接受的请求次数也是有限的
-
目的:加速数据访问,提高减轻数据库压力,利用有限的资源来提供尽可能大的吞吐量
-
场景:短时间内相同数据需要查询多次且数据更新不频繁
-
特征:
- 1.命中率;命中数/请求数
- 2.最大元素;可以存放的最大数量,如果超过最大数量则会触发清空策略
- 3.清空策略;
- LRU:最近最少使用:数据实效性
- FIFO 先进先出:高频数据
- LFU 最少使用:优先保证热点数据的有效性
-
1.2 分类
-
存储介质
- 内存
- 硬盘
- 数据库
-
与应用耦合情况
-
1.本地缓存,存在同一个进程内,访问快,无网络开销。与应用耦合,不同节点应用无法共享
如HashMap -
2 分布式缓存,与应用隔离,独立的应用,部署多点,多个不同节点应用都可以访问到。如redis memcache
-
本地缓存说明:
public class UseLocalCache {
//声明一个成员/局部变量
Map<String, Object> localCacheMap = new HashMap<String, Object>();
public Object put(String key,Object value){
Object o = localCacheMap.put(key,value);
return o;
}
public Object get(String key){
Object o = localCacheMap.get(key);
if(o == null) {
o = getInfoFromDB(key);
}
return o;
}
//示例数据库IO获取
private Object getInfoFromDB(String key) {
return new Object();
}
}
public class UseLocalCache {
private static final HttpClient httpClient = ServerHolder.createClientWithPool();
//声明一个静态变量
private static Map<Integer, String> orgCodeMap = new HashMap<Integer, String>();
static {
HttpGet get = new HttpGet("http://fudata.cn/api/org/code/all");
BaseAuthorizationUtils.generateAuthAndDateHeader(get,
BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
try {
String resultStr = httpClient.execute(get, new BasicResponseHandler());
JSONObject resultJo = new JSONObject(resultStr);
JSONArray dataJa = resultJo.getJSONArray("data");
for (int i = 0; i < dataJa.length(); i++) {
JSONObject itemJo = dataJa.getJSONObject(i);
orgCodeMap.put(itemJo.getInt("code"), itemJo.getString("name"));
}
} catch (Exception e) {
throw new RuntimeException("Init Org code Error!", e);
}
}
public static String getOrgName(int orgCode) {
String name = orgCodeMap.get(orgCode);
if (name == null) {
name = "未知";
}
return name;
}
}
本地缓存框架
-
Guava Cache
-
Ehcache
画外音:引入缓存就是为了减少数据库IO操作次数,加速数据访问。
- 2 缓存使用注意事项
-
2.1 先操作缓存还是先操作数据库
- 读操作:先读缓存,缓存命中则返回。缓存不命中,尝试从数据库里读取再加载到缓存中,方便下次读取
- 写操作:
- 【写写并发有问题】先操作数据库,再更新缓存
- 【写写并发有问题】先更新缓存,再操作数据库
- 【读写并发有问题】先淘汰缓存,再操作数据库
- 先操作数据库,再淘汰缓存
-
2.2 并发更新的大坑
- 场景:对接三方数据源把token放入缓存,需要每次带上token去调用接口,并设置失效时间,如果缓存失效时服务节点会请求三方token接口,再把获取到的token放入缓存中。
-
graph LR
serviceA-- 1 --> redis
serviceB-- 2 -->redis
serviceA-- 3 --> 三方
serviceB-- 4 --> 三方
(1)serviceA取旧token,访问接口,发现token过期;
(2)serviceB并发请求,取旧token,访问接口,也发现token过期;
(3)serviceA去申请新token1;
(4)serviceB并发申请新token2(此时token1会过期);
(5)serviceA把token1放入缓存,带着过期的token1去请求接口发现token1已过期,又去申请新的token3(此时token2会过期)
(6)serviceB把token2放入缓存,带着过期的token2去请求接口发现token2已过期,又去申请新的token4(此时token3会过期)
…
高并发请求导致相互失效。
-
解决1
graph LR
serviceA-- 1 --> redis
serviceB-- 2 -->redis
tokenService – 异步定期更新 -->redis
back --> tokenService
> 线上serviceA和serviceA只从缓存读取token
>
> 更新token异步,tokenService定期更新token,避免并发更新
> 使用tokenServiceback保证token更新高可用,tokenService挂了,back顶上
>
- 解决2
- redis分布式锁
```java
public String get(key) {
String value = redis.get(key);
//代表缓存值过期
if (value == null) {
//设置1min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 1 * 60) == 1) { //代表设置成功
value = getRemoteToken();
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else {
//这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
//重试
get(key);
}
} else {
return value;
}
}
```
- 3 手写实现LRU缓存
graph LR
NodeA-- next–>NodeB
NodeB-- pre–>NodeA
NodeB-- next–>NodeC
NodeC-- pre–>NodeB
```java
package demo.lru;
import java.util.HashMap;
import java.util.Map;
public class LinkNodeLru {
private Node head;
private Node tail;
Map<Integer, Node> map;
private int capacity;
LinkNodeLru(int capacity) {
this.capacity = capacity;
//定义头结点,不存值,作用方便结点的插入和删除
//双链表,head始终指向头结点,tail始终指向最后一个结点
this.tail = this.head = new Node(-2, -2);
//map,键是结点的属性key,值是指向结点的指针
//map作用保证了O(1)时间内实现get与put操作
this.map = new HashMap<>();
}
private int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
}
//1.当前node节点引用变更
node.pre.next = node.next;
if (node.next == null) {
this.tail = tail.pre;
} else {
node.next.pre = node.pre;
}
//2.将node放置在head节点之后(访问节点前移)
node.next = head.next;
node.pre = head;
if (node.next == null) {
this.tail = node;
} else {
node.next.pre = node;
}
head.next = node;
return node.val;
}
private Node put(int key, int val) {
Node node = map.get(key);
if (node != null) {
//1.改变当前node的值
node.val = val;
//2.当前node节点引用变更
node.pre.next = node.next;
if (node.next == null) {
this.tail = tail.pre;
} else {
node.next.pre = node.pre;
}
//3.将node放置在head节点之后(访问节点前移)
node.next = head.next;
node.pre = head;
if (node.next == null) {
this.tail = node;
} else {
node.next.pre = node;
}
head.next = node;
} else {
node = new Node(key, val);
if (map.size() == 0) {
this.tail = node;
node.pre = head;
node.next = null;
head.next = node;
} else {
node.pre = head;
node.next = head.next;
node.next.pre = node;
head.next = node;
}
if (map.size() >= capacity) {
map.remove(tail.key);
this.tail = tail.pre;
tail.next = null;
}
}
return this.map.put(key, node);
}
class Node {
private int key;
private int val;
private Node pre;
private Node next;
Node(int key, int val) {
//定义双链表的结点
this.key = key;
this.val = val;
this.pre = this.next = null;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", val=" + val +
'}';
}
}
public static void main(String[] args) {
LinkNodeLru linkNodeLru = new LinkNodeLru(1);
linkNodeLru.put(1, 2);
linkNodeLru.put(2, 3);
System.out.println(linkNodeLru.map.size());
System.out.println(linkNodeLru.map);
//1
//{2=Node{key=2, val=3}}
}
}