知识管理——从缓存击穿谈起

最近对编程知识点进行管理,发现关键还是知识的关联,就是理清知识的关系,对知识点建立关联,从而搭建知识体系。

以高并发系统中常见的缓存击穿作为例子记录。

缓存击穿,指的是当请求落到缓存时,缓存失效,请求穿过缓存直接访问数据库。

解决缓存击穿的方法,关键在于缓存失效时缓存要如何更新,保证缓存是有效的。

解决方法有两个,关键点分别是锁和异步,目的都是为了保证并发下单线程的写操作:

一是使用互斥锁,使用锁写缓存。当缓存失效时,只允许一个线程从数据库加载数据,更新缓存,其他线程只能等待获取缓存;

二是设置缓存永不过期,或者异步线程不断更新缓存,设置失效时间。

方法一的关键是锁,当多个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;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值