1. 起步
缓存是在实际生产中非常常用的工具,用了缓存以后,我们可以避免重复的计算,提高吞吐量;
缓存乍一看简单,好像使用Map
就可以实现,而初级的缓存的确是使用Map
实现的,不过一个功能完备,性能强劲的缓存,就需要考虑更多的点了,先来看看使用最简单的HashMap
public class SimpleCache {
private final HashMap<String, Integer> cache = new HashMap<>();//不可变 安全
public Integer computer(String userId) throws InterruptedException {
Integer result = cache.get(userId);
//先检查HashMap里面有没有保存,有则直接取出
if(result == null){
//缓存中找不到 那么需要去数据库找
result = getFromDb(userId);
//再放到缓存供下次使用
cache.put(userId, result);
}
return result;
}
public Integer getFromDb(String userId) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
//..find in db
return new Integer(userId);
}
}
这样实现存在问题,最大的问题就是线程不安全,解决这个问题可以在方法上同步
public class SimpleCache {
private final HashMap<String, Integer> cache = new HashMap<>();
public synchronized Integer computer(String userId) throws InterruptedException {
Integer result = cache.get(userId);
//先检查HashMap里面有没有保存,有则直接取出
if(result == null){
//缓存中找不到 那么需要去数据库找
result = getFromDb(userId);
//再放到缓存供下次使用
cache.put(userId, result);
}
return result;
}
public Integer getFromDb(String userId) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
//..find in db
return new Integer(userId);
}
}
使用Synchronized
虽然保证了线程安全,但是又带来了下面的问题
- 性能差:多个线程不能同时访问,这和缓存的使用场景非常不符,缓存正式为了在高并发并发读取的情况下使用,而使用
Synchronized
串行化执行就不再是并发读取了 - 代码复用性差,在我们实际开发中肯定是多个
service
都要使用缓存,多个service
又是不同的类,对缓存使用的相同流程在每个service中都要写一遍,代码复用性差
2. 先来解决复用性问题 – 装饰者模式
上面说说到我们多个业务都要使用缓存,那么我们要在具体的业务中都加上缓存操作,这样缓存侵入了我们的具体业务,不仅繁琐,而且如果要修改缓存逻辑就要在所有的业务中都修改一次,代码的耦合度很高
为了解决耦合度问题,我们可以使用装饰者模式,装饰者模式在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式,在对象的扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则将具体业务和缓存的实现相分离,使用的时候为具体业务装饰一下加上缓存就行了
这里只有一个具体装饰时,所以将抽象装饰和具体装饰合并
抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象
/**
* 数据库操作接口,用来代表耗时的数据库操作,每个具体的数据库操作都要实现这个接口,达到无侵入的实现缓存功能的目的
*/
public interface Dbable<A,V> {
V getFromDb(A key) throws InterruptedException ;
}
具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责
/**
* 耗时的数据库操作类,实现了Dbable接口,但是本身不具备缓存能力,不需要考虑缓存的事
*/
public class DbFunction implements Dbable<String, Integer> {
@Override
public Integer getFromDb(String key) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
//..find in db
return new Integer(key);
}
}
具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任
/**
* 装饰者模式将缓存与数据库的操作解耦
* @param <A>
* @param <V>
*/
public class ElementaryCache<A, V> implements Dbable<A, V>{
private final Map<A,V> cache = new HashMap<>();
private final Dbable<A,V> c;
public ElementaryCache(Dbable<A, V> c) {
this.c = c;
}
@Override
public synchronized V getFromDb(A key) throws InterruptedException {
//进入缓存机制
V result = cache.get(key);
//先检查HashMap里面有没有保存,有则直接取出
if(result == null){
//缓存中找不到 那么需要去数据库找
result = c.getFromDb(key);
//再放到缓存供下次使用
cache.put(key, result);
}
return result;
}
public static void main(String[] args) throws InterruptedException {
ElementaryCache<String, Integer> dbFunction = new ElementaryCache<>(new DbFunction());
Integer id = dbFunction.getFromDb("id");
}
}
3. 解决Synchronized
带来的性能问题
在上面我们对于ElementaryCache
中的整个getFromDb
都上了锁,这样两个线程不能同时访问这个方法,假设现在一个线程正在进行耗时的数据库操作,而另一个线程要去缓存中拿值,并不存在线程安全问题,但是由于synchronized
的存在,两个线程只能串行执行,性能差
解决方法就是要去减小锁的粒度,使用ConcurrentHashMap
就行了
/**
* 装饰者模式将缓存与数据库的操作解耦
* @param <A>
* @param <V>
*/
public class ElementaryCache<A, V> implements Dbable<A, V>{
private final Map<A,V> cache = new ConcurrentHashMap<>();
private final Dbable<A,V> c;
public ElementaryCache(Dbable<A, V> c) {
this.c = c;
}
@Override
public V getFromDb(A key) throws InterruptedException {
//进入缓存机制
V result = cache.get(key);
//先检查HashMap里面有没有保存,有则直接取出
if(result == null){
//缓存中找不到 那么需要去数据库找
result = c.getFromDb(key);
//再放到缓存供下次使用
cache.put(key, result);
}
return result;
}
}
4. 使用Future
解决重复查询问题
对于上面的实现看似完美,但是存在一个重复查询的问题,具体问题如下:
线程A查询Key为A的数据,这个时候缓存中没有,那么他就要去数据库中进行耗时的查找工作,当他还没查完的时候B线程来了,也要查找Key为A的数据,那么也会去数据库查找,然而这个时候线程A已经再查找了,这就是所谓的重复查询问题
使用Future
和Callable
解决问题:前人种树,后人乘凉
我们要想实现前人种树,后人乘凉我们新进来的任务要能判断前面是不是有人正在执行自己的任务,所以我们可以改变Map,private final Map<A, Future<V>> cache = new ConcurrentHashMap<>()
,这样任务会被存储起来,就能让别人判断
/**
* 装饰者模式将缓存与数据库的操作解耦
* @param <A>
* @param <V>
*/
public class ElementaryCache<A, V> implements Dbable<A, V>{
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Dbable<A,V> c;
public ElementaryCache(Dbable<A, V> c) {
this.c = c;
}
@Override
public V getFromDb(A key) throws InterruptedException, ExecutionException {
//进入缓存机制
Future<V> future = cache.get(key);
//先检查HashMap里面有没有这个任务,有则直接取出
if(future == null){
//缓存中找不到 那么需要去数据库找
FutureTask<V> ft = new FutureTask<>(new Callable<V>() {
@Override
public V call() throws Exception {
return c.getFromDb(key);
}
});
future = ft;
//把任务存在缓存
cache.put(key, ft);
ft.run();
}
//会阻塞
return future.get();
}
}
5. 使用原子组合操作填补漏洞
针对上面的改进实际上还存在重复查找的问题,这次重复查找发生在A线程进行到往map中放任务前,B线程进行到判断Future是否为null,这样由于A还没有把创建的任务放到map中,导致B以为前面没有和自己一样的任务,那么就会重复计算
这样两个线程同时启动,绕过了null判断,B也会进入if语句块,只要进入了if语句块就会创建任务,放入map,进行计算,需要在放入map时,判断map中是否已经有了这个任务,有则不去执行,这点通过map的原子操作可以达到;使用map.putIfAbsent
原子操作,在放入前判断里面有没有,由于这是线程安全的,AB线程不能同时执行这个语句,所以可以达到目的
@Override
public V getFromDb(A key) throws InterruptedException, ExecutionException {
//进入缓存机制
Future<V> future = cache.get(key);
//先检查HashMap里面有没有保存,有则直接取出
if(future == null){
//缓存中找不到 那么需要去数据库找
FutureTask<V> ft = new FutureTask<>(new Callable<V>() {
@Override
public V call() throws Exception {
return c.getFromDb(key);
}
});
future = cache.putIfAbsent(key, ft);
if(future == null){
future = ft;
ft.run();
}
}
return future.get();
}
6. 缓存过期功能
处于安全考虑,缓存需要设置有效期,到期自动失效,如果一直不失效可能会存在缓存不一致问题
/**
* 装饰者模式将缓存与数据库的操作解耦
* @param <A>
* @param <V>
*/
public class ElementaryCache<A, V> implements Dbable<A, V>{
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Dbable<A,V> c;
public ElementaryCache(Dbable<A, V> c) {
this.c = c;
}
@Override
public V getFromDb(A key) throws InterruptedException, ExecutionException {
//进入缓存机制
Future<V> future = cache.get(key);
//先检查HashMap里面有没有保存,有则直接取出
if(future == null){
//缓存中找不到 那么需要去数据库找
FutureTask<V> ft = new FutureTask<>(new Callable<V>() {
@Override
public V call() throws Exception {
return c.getFromDb(key);
}
});
future = cache.putIfAbsent(key, ft);
if(future == null){
future = ft;
ft.run();
}
}
return future.get();
}
public final static ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
/**
* 使用定时线程池可以实现定时清除缓存的功能
* @param key
* @param expire 缓存到期时间
* @return
*/
public V getFromDb(A key, long expire) throws ExecutionException, InterruptedException {
if(expire>0){
service.schedule(new Runnable() {
@Override
public void run() {
expire(key);
}
},expire, TimeUnit.MICROSECONDS);
}
return getFromDb(key);
}
/**
* 清除缓存
* @param key
*/
public synchronized void expire(A key){
Future<V> future = cache.get(key);
if(future!=null){
if(!future.isDone()){
//取消任务
future.cancel(true);
}
//清除缓存
cache.remove(key);
}
}
}
但是我们不能让过期的时间一致,不然会有缓存雪崩问题,需要把缓存的时间设置为随机的,再包装一层,让缓存失效的时间随机
public V getFromDbRandomExpire(A key) throws ExecutionException, InterruptedException {
long randomExpire = (long)(Math.random()*10000);
return getFromDb(key, randomExpire);
}