本地缓存:为什么要用本地缓存?用它会有什么问题? | 您所在的位置:网站首页 › apex都有什么服务器 › 本地缓存:为什么要用本地缓存?用它会有什么问题? |
背景
在高性能的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,我们通常会将一些热点数据存储到Redis或Memcached 这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能降低数据库的压力。 随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构。 在先不考虑并发等复杂问题的情况下,两级缓存的访问流程可以用下面这张图来表示: 缓存的本质就是存储在内存中的KV数据结构,对应的就是jdk中线程安全的ConcurrentHashMap,但是要实现缓存,还需要考虑淘汰、最大限制、缓存过期时间淘汰等等功能; 优点是实现简单,不需要引入第三方包,比较适合一些简单的业务场景。缺点是如果需要更多的特性,需要定制化开发,成本会比较高,并且稳定性和可靠性也难以保障。对于比较复杂的场景,建议使用比较稳定的开源工具。 2. 基于Guava Cache实现本地缓存Guava是Google团队开源的一款 Java 核心增强库,包含集合、并发原语、缓存、IO、反射等工具箱,性能和稳定性上都有保障,应用十分广泛。Guava Cache支持很多特性: 支持最大容量限制支持两种过期删除策略(插入时间和访问时间)支持简单的统计功能基于LRU算法实现使用代码如下: com.google.guava guava 31.1-jre @Slf4j public class GuavaCacheTest { public static void main(String[] args) throws ExecutionException { Cache cache = CacheBuilder.newBuilder() .initialCapacity(5) // 初始容量 .maximumSize(10) // 最大缓存数,超出淘汰 .expireAfterWrite(60, TimeUnit.SECONDS) // 过期时间 .build(); String orderId = String.valueOf(123456789); // 获取orderInfo,如果key不存在,callable中调用getInfo方法返回数据 String orderInfo = cache.get(orderId, () -> getInfo(orderId)); log.info("orderInfo = {}", orderInfo); } private static String getInfo(String orderId) { String info = ""; // 先查询redis缓存 log.info("get data from redis"); // 当redis缓存不存在查db log.info("get data from mysql"); info = String.format("{orderId=%s}", orderId); return info; } } 3. CaffeineCaffeine是基于java8实现的新一代缓存工具,缓存性能接近理论最优。可以看作是Guava Cache的增强版,功能上两者类似,不同的是Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,在性能上有明显的优越性 使用代码如下: com.github.ben-manes.caffeine caffeine 2.9.3 @Slf4j public class CaffeineTest { public static void main(String[] args) { Cache cache = Caffeine.newBuilder() .initialCapacity(5) // 超出时淘汰 .maximumSize(10) //设置写缓存后n秒钟过期 .expireAfterWrite(60, TimeUnit.SECONDS) //设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite //.expireAfterAccess(17, TimeUnit.SECONDS) .build(); String orderId = String.valueOf(123456789); String orderInfo = cache.get(orderId, key -> getInfo(key)); System.out.println(orderInfo); } private static String getInfo(String orderId) { String info = ""; // 先查询redis缓存 log.info("get data from redis"); // 当redis缓存不存在查db log.info("get data from mysql"); info = String.format("{orderId=%s}", orderId); return info; } } 4. EhcacheEhcache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。同Caffeine和Guava Cache相比,Ehcache的功能更加丰富,扩展性更强: 支持多种缓存淘汰算法,包括LRU、LFU和FIFO缓存支持堆内存储、堆外存储、磁盘存储(支持持久化)三种支持多种集群方案,解决数据共享问题使用代码如下: org.ehcache ehcache 3.9.7 @Slf4j public class EhcacheTest { private static final String ORDER_CACHE = "orderCache"; public static void main(String[] args) { CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder() // 创建cache实例 .withCache(ORDER_CACHE, CacheConfigurationBuilder // 声明一个容量为20的堆内缓存 .newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(20))) .build(true); // 获取cache实例 Cache cache = cacheManager.getCache(ORDER_CACHE, String.class, String.class); String orderId = String.valueOf(123456789); String orderInfo = cache.get(orderId); if (StrUtil.isBlank(orderInfo)) { orderInfo = getInfo(orderId); cache.put(orderId, orderInfo); } log.info("orderInfo = {}", orderInfo); } private static String getInfo(String orderId) { String info = ""; // 先查询redis缓存 log.info("get data from redis"); // 当redis缓存不存在查db log.info("get data from mysql"); info = String.format("{orderId=%s}", orderId); return info; } } 本地缓存问题及解决 1. 缓存一致性两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。 解决方案1: MQ一般现在部署都是集群部署,有多个不同节点的本地缓存; 可以使用MQ的广播模式,当数据修改时向MQ发送消息,节点监听并消费消息,删除本地缓存,达到最终一致性; 如果你不想在你的业务代码发送MQ消息,还可以适用近几年比较流行的方法:订阅数据库变更日志,再操作缓存。Canal 订阅Mysql的 Binlog日志,当发生变化时向MQ发送消息,进而也实现数据一致性。 ![]() 对于本地缓存的方案中,我比较推荐Caffeine,性能上遥遥领先。虽然Ehcache功能更为丰富,甚至提供了持久化和集群的功能,但是这些功能完全可以依靠其他方式实现。真实的业务工程中,建议使用Caffeine作为本地缓存,另外使用redis或者memcache作为分布式缓存,构造多级缓存体系,保证性能和可靠性。 |
CopyRight 2018-2019 实验室设备网 版权所有 |