• 缓存技术内部交流_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 操作后对缓存执行移除操作而不是更新操作就可以了。

  • 相关阅读:
    ADF中遍历VO中的行数据(Iterator)
    程序中实现两个DataTable的Left Join效果(修改了,网上第二个DataTable为空,所处的异常)
    ArcGIS api for javascript——鼠标悬停时显示信息窗口
    ArcGIS api for javascript——查询,然后单击显示信息窗口
    ArcGIS api for javascript——查询,立刻打开信息窗口
    ArcGIS api for javascript——显示多个查询结果
    ArcGIS api for javascript——用图表显示查询结果
    ArcGIS api for javascript——查询没有地图的数据
    ArcGIS api for javascript——用第二个服务的范围设置地图范围
    ArcGIS api for javascript——显示地图属性
  • 原文地址:https://www.cnblogs.com/gordonkong/p/7161754.html
Copyright © 2020-2023  润新知