最近对编程知识点进行管理,发现关键还是知识的关联,就是理清知识的关系,对知识点建立关联,从而搭建知识体系。
以高并发系统中常见的缓存击穿作为例子记录。
缓存击穿,指的是当请求落到缓存时,缓存失效,请求穿过缓存直接访问数据库。
解决缓存击穿的方法,关键在于缓存失效时缓存要如何更新,保证缓存是有效的。
解决方法有两个,关键点分别是锁和异步,目的都是为了保证并发下单线程的写操作:
一是使用互斥锁,使用锁写缓存。当缓存失效时,只允许一个线程从数据库加载数据,更新缓存,其他线程只能等待获取缓存;
二是设置缓存永不过期,或者异步线程不断更新缓存,设置失效时间。
方法一的关键是锁,当多个http请求读缓存时,只能有一个htttp对应的线程负责写缓存。从锁可以关联的知识点,双重检测锁、分布式锁。
方法二的关键是异步,多个http请求只负责读缓存,缓存的更新和请求无关,后台会有异步线程不断写缓存。从异步可以关联,异步消费模式、AQS。
此时知识点就会变成知识线,缓存击穿 —— 分布式锁 或 异步消费 。
下面就是具体的知识点。
双重检测锁,最常见单例模式,通过双重检测对象是否为空实现
private Object mux = new Object(); // 锁
private Object instance; // 单例对象
private void init() {
if (instance == null) {
synchronized (mux) {
if (instance == null) {
instance = new Object();
}
}
}
}
分布式锁,一般使用redis或者zookeeper实现。
redis分布式锁,用set+exist/setnx和expire两命令实现,值为线程id或者是过期时间命令setnx + expire分开写的lua实现,setnx用set和exist组合代替
-- 加锁
local lockname = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];
-- 首次请求
if(redis.call('exists', lockname) == 0) then
redis.call('set', lockname, threadId);
redis.call('expire', lockname, releaseTime);
return 1;
end;
-- 重复请求(此处没记录持有锁的线程的重入次数,所以不支持可重入)
if(redis.call('exists', lockname) == 1) then
redis.call('expire', lockname, releaseTime);
return 0;
end;
return -1;
-- 解锁
local lockname = KEYS[1];
local threadId = ARGV[1];
-- lockname、threadId不存在
if (redis.call('hexists', lockname, threadId) == 0) then
return 0;
end;
redis.call('del', key);
return 1;
set的扩展命令(set ex px nx)的java实现
// 加锁
public Boolean tryLock(String lockName, String threadId, long timeout, TimeUnit unit) {
return redisTemplate.opsForValue().setIfAbsent(lockName, threadId, timeout, unit);
}
// 解锁,防止删错别人的锁,以uuid为value校验是否自己的锁
public void unlock(String lockName, String threadId) {
if(threadId.equals(redisTemplate.opsForValue().get(lockName)){
redisTemplate.opsForValue().del(lockName);
}
}
异步消费模式(使用AQS实现)
LinkedBlockingQueue<Object> eventQueue = new LinkedBlockingQueue<>();
public void push(Object event){
eventQueue.add(event);
}
public void run(){
// 异步线程
while(true){
try {
Object event = this.eventQueue.poll(3000, TimeUnit.MILLISECONDS);
// 异步消费
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在redission中,也有异步线程更新缓存的实现,那就是大名鼎鼎的看门狗watchdog
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime,
TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(waitTime, leaseTime, unit,
threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime,
this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}