zoukankan      html  css  js  c++  java
  • 缓存技术内部交流_03_Cache Aside

    参考资料:
    http://www.ehcache.org/documentation/3.2/caching-patterns.html
    http://www.ehcache.org/documentation/3.2/usermanaged.html(optional)

    示例代码:
    https://github.com/gordonklg/study,cache module

    A. 缓存模式(Caching Patterns)

    缓存模式有两种,一种是 Cache Aside,一种是 Cache-As-SoR(system-of-record)。

    在 Cache Aside 模式中,应用程序直接操作缓存与系统数据,由应用程序保证缓存的有效性。而在 Cache-As-SoR 模式中,应用程序只能看见缓存,缓存层有自己的 loader 模块,负责与系统数据的交互。

    B. Ehcache3 实现 Cache Aside 模式

    gordon.study.cache.ehcache3.pattern.CacheAsideUserService.java

        private UserManagedCache<String, UserModel> cache;
     
        public CacheAsideUserService() {
            cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(String.class, UserModel.class).build(true);
        }
     
        public UserModel findUser(String id) {
            UserModel cached = cache.get(id);
            if (cached != null) {
                System.out.println("get user from cache");
                return cached;
            }
            UserModel user = new UserModel(id, "info ..."); // find user
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("get user from db");
            cache.put(id, user);
            return user;
        }
     
        public UserModel updateUser(String id, String info) {
            UserModel user = new UserModel(id, info); // update user
            cache.put(id, user);
            return user;
        }
     
        public boolean deleteUser(String id) {
            // delete user
            cache.remove(id);
            return true;
        }
     
        public static void main(String[] args) {
            final CacheAsideUserService service = new CacheAsideUserService();
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("1");
                    }
                });
            }
            executorService.shutdown();
        }
    

    代码第1行定义了一个 user managed caches,这种 cache 不需要 CacheManager 管理。

    findUser 方法首先尝试从缓存中获取数据,如果缓存中没有相应数据,则从 SoR 中获取数据并将之放入缓存。

    updateUser 方法修改完 SoR 后会更新缓存中的数据。

    deleteUser 方法删除 SoR 中记录后会移除缓存中的数据。

    以上就是一个典型的 Cache Aside 示例。

    C. Cache Aside 模式的问题:并发读

    Cache Aside 模式存在一些问题,第一是并发读的问题,在高并发场景下,当多个请求同时访问一条数据时,如果此时数据不在缓存中(例如刚过期),则这些请求会同时去后端 SoR 中获取数据,瞬间压力巨大。

    上面示例代码 main 函数执行时会打印10条 "get user from db",表示所有线程同时访问了后端 SoR。

    解决这个问题的办法是引入同步机制,保证只会有一个线程去后端 SoR 中获取数据,其余线程等待数据进入缓存后直接从缓存获取,减轻 SoR 端的压力。

    gordon.study.cache.ehcache3.pattern.SyncCacheAsideUserService.java

        private UserManagedCache<String, UserModel> cache;
     
        private final ReentrantLock lock = new ReentrantLock();
     
        public SyncCacheAsideUserService() {
            cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(String.class, UserModel.class).build(true);
        }
     
        public UserModel findUser(String id) {
            UserModel result = cache.get(id);
            if (result == null) {
                lock.lock();
                result = cache.get(id);
                if (result == null) {
                    result = new UserModel(id, "info ..."); // find user
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("get user from db: " + id);
                    cache.put(id, result);
                } else {
                    System.out.println("get user from cache after competition: " + id);
                }
                lock.unlock();
            } else {
                System.out.println("get user from cache: " + id);
            }
            return result;
        }
    
        public static void main(String[] args) {
            final SyncCacheAsideUserService service = new SyncCacheAsideUserService();
            ExecutorService executorService = Executors.newFixedThreadPool(30);
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("1");
                    }
                });
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("2");
                    }
                });
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("3");
                    }
                });
            }
            executorService.shutdown();
        }
    

    通过加锁,保证了只有竞争 lock 成功的线程才能去后端 SoR 获取数据,其它线程只能等待。

    从 SyncCacheAsideUserService 的输出发现了新的问题:现在从 SoR 获取数据变成了串行的方式,对不同的 id 的查找操作也被 lock 锁给同步了。

    直觉上,我们会想到为每个不同的 id 使用不同的锁。为了查找效率,这些锁需要放置在 Map 结构中,通过 id 为 key 方便检索。最后,由于 id 可取值范围太广,普通的 map 会造成内存泄漏,因此考虑使用 WeakHashMap 解决内存泄漏问题,这样便产生了以下试验性代码(演示用,不要用于生产环境)

    gordon.study.cache.ehcache3.pattern.SyncByIdCacheAsideUserService.java

        private UserManagedCache<String, UserModel> cache;
     
        private final Map<String, ReentrantLock> lockMap = new WeakHashMap<>();
     
        private final ReentrantLock lockMapLock = new ReentrantLock();
     
        public SyncByIdCacheAsideUserService() {
            cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(String.class, UserModel.class).build(true);
        }
     
        public UserModel findUser(String id) {
            UserModel result = cache.get(id);
            if (result == null) {
                lockMapLock.lock();
                ReentrantLock lock = lockMap.get(id);
                if (lock == null) {
                    lock = new ReentrantLock();
                    lockMap.put(new String(id), lock);
                }
                lockMapLock.unlock();
     
                lock.lock();
                result = cache.get(id);
                if (result == null) {
                    result = new UserModel(id, "info ..."); // find user
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("get user from db: " + id);
                    cache.put(id, result);
                } else {
                    System.out.println("get user from cache after competition: " + id);
                }
                lock.unlock();
            } else {
                System.out.println("get user from cache: " + id);
            }
            return result;
        }
    
        public static void main(String[] args) throws Exception {
            final SyncByIdCacheAsideUserService service = new SyncByIdCacheAsideUserService();
            ExecutorService executorService = Executors.newFixedThreadPool(30);
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("1");
                    }
                });
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("2");
                    }
                });
                executorService.execute(new Runnable() {
                    public void run() {
                        service.findUser("3");
                    }
                });
            }
            executorService.shutdown();
            executorService.awaitTermination(5, TimeUnit.SECONDS);
            System.gc();
            Thread.sleep(1000);
            System.out.println(service.lockMap.size());
        }
    

    D. Cache Aside 模式的问题:并发读写

    当读写操作并发存在时,理论上有缓存旧数据的可能。可能场景如下:

    • 线程A读取用户,缓存未命中,于是从 SoR 中读取到旧数据
    • 线程B更新用户,将 SoR 中数据更新,然后更新缓存
    • 线程A将旧数据更新到缓存

    但是这种场景仅仅是理论上可能,因为在 SoR 中更新数据一般是比较费时的操作(而且数据库写操作会锁表,读操作必然是要在锁表前读取出旧值的),线程A没理由会在这么长的时间段内没将旧数据更新到缓存。所以一般情况下,我们会无视这种情况。

    E. Cache Aside 模式的问题:并发写

    并发写也可能导致缓存旧数据。可能场景如下:

    • 线程A更新用户到版本2
    • 线程B更新用户到版本3,然后更新缓存
    • 线程A用版本2的数据更新缓存

    同上,这种场景发生概率也很低,尤其是对于用户模块来说,很少有并发写的场景,所以可以不用太考虑,只要设定合适的缓存过期时间就可以了。

    不过,解决并发写问题的方法很简单,只要在 update 操作后对缓存执行移除操作而不是更新操作就可以了。

  • 相关阅读:
    Java ClassLoader机制
    Spring JMS
    MySQL权限分配
    Java参数传递机制
    JVM装载过程
    PowerDesigner15使用时的十五个问题
    修改当前行 传值
    WebSphere MQ
    Hibernate Search牛刀小试 (转)
    关于hibernate的缓存使用
  • 原文地址:https://www.cnblogs.com/gordonkong/p/7161754.html
Copyright © 2011-2022 走看看