最近对编程知识点进行管理,发现关键还是知识的关联,就是理清知识的关系,对知识点建立关联,从而搭建知识体系。
以高并发系统中常见的缓存击穿作为例子记录。
缓存击穿,指的是当请求落到缓存时,缓存失效,请求穿过缓存直接访问数据库。
解决缓存击穿的方法,关键在于缓存失效时缓存要如何更新,保证缓存是有效的。
解决方法有两个,关键点分别是锁和异步,目的都是为了保证并发下单线程的写操作:
一是使用互斥锁,使用锁写缓存。当缓存失效时,只允许一个线程从数据库加载数据,更新缓存,其他线程只能等待获取缓存;
二是设置缓存永不过期,或者异步线程不断更新缓存,设置失效时间。
方法一的关键是锁,当多个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
-
- 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
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
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;
- }
- }