对缓存使用的一些思考

一、使用缓存

1、缓存的使用适用于变化不频繁,需要大量被访问的数据,例如线路、仓库名称等,每次大促来临之时,业务上会禁止一些数据的变更,以便于数据预热到缓存时能与DB保持一致,大促之时,系统基本从缓存中读取数据,以此减少IO次数,同时也是为了保护高流量下DB不会击垮。

2、缓存分为本地缓存 & 分布式缓存,本地缓存是将热点数据存储到本地JVM中,系统访问数据时先从本地缓存中读取,没有再从分布式缓存中获取,先读取本地可以有效地降低网络通信的耗时(访问分布式缓存需要走网络通信)。本地缓存如今有许多开源框架供以选择,例如guava cache、Caffine等,当然最简单的还是使用JDK的ConcurrentHashMap,在高并发系统中,Caffine性能好于guava,一般系统中,使用guava即可。该模式理解为二级缓存,有效解决热点问题,本地缓存失效时间设置稍短约于分布式缓存。

3、使用缓存的场景是高并发 & 允许出现短时间的数据不一致。如果业务是实时性的,需要考虑采用主动推送的方式更新缓存,当数据库出现变化时,主动put最新值到缓存中,但update & put操作还是存在时间差的,这段时间里还是会出现缓存不一致的情况,但好于缓存失效之后再去DB中捞取数据。

二、使用缓存

1、考虑到最简单的场景:先从缓存中查询数据,没有再从DB中获取,有查询结果放入到缓存中,这种场景适用于一般流量小系统,一定不用用在高并发系统中,系统会完蛋的。上述流程存在缓存击穿的致命弱点:

  • 缓存数据同时失效
  • 恶意查询不存在数据,导致缓存命中率=0,大量请求访问DB(此时缓存基本就是摆设了)

针对缓存同时失效,可以在失效时间+随机数,使得缓存失效尽量错开,减少同时大量请求访问DB,随机数一定要使用ThreadLocalRandom,它的性能优于Random,原因在于Random获取随机数时使用了CAS+for循环,导致性能浪费。详细原理对比参考:

http://ifeve.com/%E5%B9%B6%E5%8F%91%E5%8C%85%E4%B8%ADthreadlocalrandom%E7%B1%BB%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90/

http://www.importnew.com/12460.html


public void putCache(){

    UsaTairManager.prefixPut(this.usaMcTairManager, UsaConstants.USA_TAIR_NAMESPACE,
                (UsaConstants.USA_PREFIX_KEY + pKey), sKey,          (ArrayList<UsaServiceCpResourceLineDTO>)tairValue, 0,
                getPlusExpireTime());
}

 private int getPlusExpireTime(){
        return ThreadLocalRandom.current().nextInt(100);
}

针对查询不存在的数据,当第一次查询缓存未命中走到DB端时,返回的是NULL,此时依然要放入一个空对象进入缓存(不是NULL),例如NullObject。



import java.io.Serializable;
import java.util.Objects;


public class NullObject implements Serializable {

    /**
     * 一定要个字段,不然equals无法实现
     */
    private Boolean dummy = false;

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        NullObject that = (NullObject)o;
        return Objects.equals(dummy, that.dummy);
    }

    @Override
    public int hashCode() {
        return Objects.hash(dummy);
    }
}

使用该NullObject方式如下:


import java.io.Serializable;



public class DefaultCacheClient implements CacheClient {

    private static final NullObject NULL = new NullObject();

    private Cache cache;

    private boolean enabled = true;

    public void setCache(Cache cache) {
        this.cache = cache;
    }

    public Cache getCache() {
        return this.cache;
    }

    @Override
    public void put(String key, Serializable value) {
        if (enabled) {
            if (null != value) {
                cache.put(key, value);
            } else {
                cache.put(key, NULL);
            }
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        if (!enabled) {
            return null;
        }

        Serializable v = cache.get(key);
        if (null != v && v instanceof NullObject) {
            return null;
        }
        return (T)v;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(String key, ValueGenerator generator) {
        if (!enabled) {
            return (T)generator.generate();
        }

        Serializable v = cache.get(key);
        if (null == v) {
            v = generator.generate();
            if (v != null) {
                cache.put(key, v);
            } else {
                cache.put(key, NULL);
            }
        } else if (v instanceof NullObject) {
            v = null;
        }
        return (T)v;
    }

}

除了上面提到的2点,还有一个解决方案是锁机制:

当多个线程同时携带同一个key查询时,未命中时,对请求的key竞争锁,让第一个查询线程拿到这个锁,访问DB并返回给Cache,其他线程则不断查询cache,如果未命中则再次尝试获取锁,一旦获取到,再查询一次cache,即双重判断,重复循环直至缓存命中或拿到锁。对key加锁,synchronized(key)是不行的,因为不同线程请求的key是equals的但不一定是同一个对象,所以借助guava的本地cache做锁比较合适。

 private static Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(1000000)
            .expireAfterWrite(expire, TimeUnit.SECONDS).build();
    /**
     * 锁缓存,支持add and get原子操作
     */
    private static LoadingCache<String, AtomicInteger> lockCache = CacheBuilder.newBuilder()
            .maximumSize(1000000)
            .expireAfterWrite(lockExpire, TimeUnit.SECONDS)
            .build(new CacheLoader<String, AtomicInteger>() {
                @Override
                public AtomicInteger load(String key) throws Exception {
                    return new AtomicInteger(0);
                }
            });


   public static String getConfig(final String key) {
        String result;
        Preconditions.checkArgument(StringUtils.isNotBlank(key));
        try {
            boolean access = false;
            long start = System.currentTimeMillis();
            // 等待cache生效有超时时间
            while (!access && (System.currentTimeMillis() - start) < timeout) {
                // 如果缓存命中直接返回
                result = cache.getIfPresent(key);
                if (result != null) {
                    return result;
                }
                // 获取锁
                AtomicInteger lockValue = lockCache.get(key);
                // add and get = 1表示获取到锁
                access = (lockValue.addAndGet(1) == 1);
                if (access) {
                    try {
                        //  获取到锁后双重判断,有可能获取锁前未命中,获取到后其他线程已经put cache
                        result =  cache.getIfPresent(key);
                        if (result != null) {
                            return result;
                        }
                        // 从diamond获取配置
                        result = getFromDB(key);
                        if (result != null) {
                            // 缓存配置
                            cache.put(key, result);
                        }
                        return result;
                    } catch (Exception e) {
                        throw new RuntimeException("error getting diamond config", e);
                    } finally {
                        // 获取到锁的保证释放锁
                        lockCache.invalidate(key);
                    }
                } else {
                    try {
                        //等待
                        Thread.sleep(10);
                    } catch (Exception e) {
                        // ignore
                    }
                }
            }
            throw new RuntimeException("wait for lock timeout");
        } catch (Exception e) {
            throw new RuntimeException("error getting lock", e);
        }
    }

2、针对于非击穿式缓存,请求从缓存中查询,无论有没有数据都会直接返回,避免了访问DB造成的缓存击穿问题,那么如果查询不到数据怎么办呢,使用异步(队列+单线程),把不存在value的key放入队列中,一个线程负责从队列中取出key——查询DB——放入缓存。由于是单线程,即使访问DB压力也不大。劣势在于会导致一定时间内的数据不一致。


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;


public class DatabaseDefender {

    private final ExecutorService threadPool = Executors.newFixedThreadExecutor(2);

    private final BlockingQueue<Item> queue = new LinkedBlockingQueue<Item>(100);

    private volatile boolean isInited = false;

    public void defend(Long sellerId, String spCode) {
        Item item = new Item(sellerId, spCode);
        boolean isAdd = queue.offer(item);
        if (!isAdd) {
            //log
        } else {
            if (! isInited) {
                synchronized (queue) {
                    if (! isInited) {
                        threadPool.execute(new Runnable() {
                            @Override
                            public void run() {
                                try {

                                    for (; ; ) {
                                        Item item = queue.take();
                                        //get from db then put it into cache
                                    }
                                } catch (Throwable e) {
                                   
                                }
                            }
                        });
                    }
                }
                isInited = true;
            }
        }
    }

}

 

缓存更新的其他知识可以参考https://coolshell.cn/articles/17416.html

转载于:https://my.oschina.net/u/2302503/blog/1819280

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值