微服务已成为软件开发领域的一种变革性架构方法,提供了从整体结构到更加模块化和可扩展的系统的范式转变。微服务的核心是将复杂的应用程序分解为更小的、可独立部署的服务,这些服务可以无缝通信,从而提高敏捷性、灵活性和易维护性。这种分散的方法使开发人员能够专注于特定功能,从而实现快速开发、持续集成和高效扩展,以满足现代动态业务环境的需求。随着组织越来越多地接受微服务的好处,本文探讨了与这种架构风格相关的关键原则、优势和挑战,阐明了它在塑造软件设计和部署的未来中的关键作用。
微服务应用程序的一个基本特征是能够利用不同的技术堆栈独立设计、开发和部署每个微服务。每个微服务都作为一个独立的自治应用程序运行,具有自己的专用持久性存储,无论是关系数据库、NoSQL 数据库,甚至是传统文件存储系统。这种自主性使单个微服务能够独立扩展,从而促进无缝的实时基础设施调整并增强整体可管理性。
NCache 微服务架构中的缓存层
在应用程序事务激增的情况下,瓶颈可能会持续存在,尤其是在微服务将数据存储在不可扩展的关系数据库中的架构中。简单地部署微服务的其他实例并不能缓解问题。
为了应对这些挑战,请考虑集成 NCache 作为分布式缓存在微服务和数据存储之间的缓存层。NCache 不仅用作缓存,还用作可扩展的内存中发布者/订阅者消息传递代理,促进微服务之间的异步通信。
微服务 Java 应用程序性能优化可以通过缓存技术来实现,例如 缓存项锁定、分组缓存数据、休眠缓存、SQL 查询、数据结构、spring 数据缓存技术、发布-订阅消息传递等等 NCache.请检查开箱即用的功能 NCache.
使用 NCache 作为 Hibernate 二级 Java 缓存
Hibernate 一级缓存
Hibernate 第一级缓存充当链接到 Session 对象的基本独立(进程内)缓存,仅限于当前会话。尽管如此,第一级缓存的一个缺点是它无法在不同会话之间共享对象。如果多个会话需要同一个对象,则每个会话都会触发数据库行程来加载它,从而增加数据库流量并加剧可扩展性问题。此外,当会话结束时,所有缓存的数据都将丢失,因此需要在下次检索时从数据库中重新获取。
Hibernate 二级缓存
对于仅依赖于第一级缓存的高流量 Hibernate 应用程序,在 Web 场中部署会带来与跨服务器缓存同步相关的挑战。在 Web 场设置中,每个节点都运行一个 Web 服务器(如 Apache、Oracle WebLogic 等),其中包含多个 httpd 进程实例来为请求提供服务。这些 HTTP 工作进程中的每个 Hibernate 一级缓存都维护着直接从数据库缓存的相同数据的不同版本,这会带来同步问题。
这就是 Hibernate 提供具有提供程序模型的二级缓存的原因。Hibernate 二级缓存使您能够集成第三方分布式 (out-proc) 缓存提供程序,以跨会话和服务器缓存对象。与第一级缓存不同,第二级缓存与 SessionFactory 对象相关联,并且可供整个应用程序访问,扩展到单个会话之外。
启用 Hibernate 二级缓存会导致两个缓存共存:一级缓存和二级缓存。Hibernate 首先尝试从第一级缓存中检索对象;如果不成功,它会尝试从二级缓存中获取它们。如果两次尝试都失败,则直接从数据库加载对象并缓存对象。此配置大大减少了数据库流量,因为很大一部分数据由二级分布式缓存提供。
NCache Java 通过扩展实现了 Hibernate 二级缓存提供程序 org.hibernate.cache.CacheProvider.集成 NCache Java Hibernate 分布式缓存提供程序与 Hibernate 应用程序不需要更改代码。这种集成使您能够将 Hibernate 应用程序扩展到多服务器配置,而不会使数据库成为瓶颈。NCache 还提供企业级分布式缓存功能,包括数据大小管理、跨服务器数据同步等。
要合并 NCache Java Hibernate 缓存提供程序,只需对 hibernate.cfg.xml 和 ncache.xml 进行简单修改即可。
因此,随着 NCache Java Hibernate 分布式缓存提供程序,您可以无缝地实现 Hibernate 应用程序的线性可扩展性,无需更改现有代码。
代码片段
1 // Configure Hibernate properties programmatically 2 Properties hibernateProperties = new Properties(); 3 hibernateProperties.put("hibernate.connection.driver_class", "org.h2.Driver"); 4 hibernateProperties.put("hibernate.connection.url", "jdbc:h2:mem:testdb"); 5 hibernateProperties.put("hibernate.show_sql", "false"); 6 hibernateProperties.put("hibernate.hbm2ddl.auto", "create-drop"); 7 hibernateProperties.put("hibernate.cache.use_query_cache", "true"); 8 hibernateProperties.put("hibernate.cache.use_second_level_cache", "true"); 9 hibernateProperties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.jcache.internal.JCacheRegionFactory"); 10 hibernateProperties.put("hibernate.javax.cache.provider", "com.alachisoft.ncache.hibernate.jcache.HibernateNCacheCachingProvider"); 11 // Set other Hibernate properties as needed 12 Configuration configuration = new Configuration() 13 .setProperties(hibernateProperties).addAnnotatedClass(Product.class); 14 15 Logger.getLogger("org.hibernate").setLevel(Level.OFF); 16 17 // Build the ServiceRegistry 18 ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() 19 .applySettings(configuration.getProperties()).build(); 20 21 // Build the SessionFactory 22 SessionFactory factory = configuration.buildSessionFactory(serviceRegistry); 23 24 // Create a List of Product objects 25 ArrayList<Product> products = (ArrayList<Product>) getProducts(); 26 // Open a new Hibernate session to save products to the database. This also caches it 27 try (Session session = factory.openSession()) { 28 Transaction transaction = session.beginTransaction(); 29 // save() method saves products to the database and caches it too 30 System.out.println("ProductID, Name, Price, Category"); 31 for (Product product : products) { 32 System.out.println("- " + product.getProductID() + ", " + product.getName() + ", " + product.getPrice() + ", " + product.getCategory()); 33 session.save(product); 34 } 35 transaction.commit(); 36 System.out.println(); 37 38 // Now open a new session to fetch products from the DB. 39 // But, these products are actually fetched from the cache 40 try (Session session = factory.openSession()) { 41 List<Product> productList = (List<Product>) session.createQuery("from Product").list(); 42 if (productList != null) { 43 printProductDetails(productList); 44 } 45 } 46
集成 NCache 与 Hibernate 轻松缓存查询结果。当 Hibernate 随后获取这些对象时,它们将从缓存中检索,从而避免了昂贵的数据库之旅。
从上面的代码示例中,产品保存在数据库中,并且还进行了缓存;现在,当新会话打开以获取产品详细信息时,它将从 Cache 中获取并避免不必要的数据库访问。
了解有关 Hibernate 缓存的更多信息
扩展与 NCache 发布/订阅消息
NCache 是专为 .NET 设计的分布式内存缓存解决方案。它的兼容性通过本机客户端和第三方集成扩展到 Java,确保对这两个平台的无缝支持。
NCache 作为为 .NET 和 Java 量身定制的内存分布式数据存储,为事件驱动的通信提供功能丰富的内存发布/订阅机制。这使得设置变得简单 NCache 作为消息传递代理,采用 Pub/Sub 模型实现微服务之间的无缝异步通信。
使用 NCache 内存中的 Pub/Sub 用于微服务
NCache 通过建立微服务可以发布和订阅事件的主题来启用 Pub/Sub 功能。这些事件发布到 NCache 微服务外的消息代理。在每个订阅微服务中,都有一个事件处理程序,用于在原始微服务发布相应事件后对其进行管理。
在 Java 微服务领域, NCache 充当事件总线或消息代理,促进将消息中继到一个或多个订阅者。
在需要通信渠道的 Pub/Sub 模型的上下文中, NCache 作为主题的媒介。这需要发布者将消息调度到指定的主题,而订阅者通过同一主题接收通知。采用 NCache 作为主题的媒介促进了模型内的松散耦合,为分布式主题提供了增强的抽象和额外的优势。
发布
下面的代码片段初始化 messageService 对象 NCache MessagingService 包。
初始化 Topic
1 // Create a Topic in NCache. 2 MessagingService messagingService = cache.getMessagingService(); 3 Topic topic = messagingService.createTopic(topicName); 4 5 // Create a thread pool for publishers 6 ExecutorService publisherThreadPool = Executors.newFixedThreadPool(2); 7 8 The below code snippet used to define register the subscribers to this topic 9 Register subscribers to this Topic 10 MessageReceivedListener subscriptionListener1 = new MessageReceivedListener() { 11 @Override 12 public void onMessageReceived(Object o, MessageEventArgs messageEventArgs) { 13 messageReceivedSubscription1(messageEventArgs.getMessage()); 14 } 15 }; 16 MessageReceivedListener subscriptionListener2 = new MessageReceivedListener() { 17 @Override 18 public void onMessageReceived(Object o, MessageEventArgs messageEventArgs) { 19 messageReceivedSubscription2(messageEventArgs.getMessage()); 20 } 21 }; 22 TopicSubscription subscription1 = topic.createSubscription(subscriptionListener1); 23 TopicSubscription subscription2 = topic.createSubscription(subscriptionListener2); 24
NCache 提供两种持久订阅变体,以满足 Java 微服务中的消息持久性需求:
共享持久订阅:这允许多个订阅者连接到单个订阅。采用 Round Robin 方法在各种订阅者之间分发消息。即使订阅者退出网络,消息也会在活动订阅者之间持续流动。
Exclusive Durable Subscriptions:在此类型中,在任何给定时间,一个订阅都只允许一个活动订阅者。在现有连接处于活动状态之前,不接受同一订阅的新订阅者请求。
了解更多 发布/订阅消息收发 NCache 在这里实施缓存中的 Pub/Sub 消息收发:概述
缓存上的 SQL 查询
NCache 为您的微服务提供了对索引缓存数据执行类似 SQL 的查询的能力。当存储所需信息的键的值未知时,此功能将变得特别有用。它抽象了许多较低级别的缓存 API 调用,有助于实现更清晰、更易于维护的应用程序代码。对于发现类似 SQL 的命令更直观、使用起来更舒适的人来说,此功能尤其有利。
NCache 提供通过类似于 SQL 的 SELECT 和 DELETE 语句的查询来搜索和删除缓存数据的功能。但是,INSERT 和 UPDATE 等操作不可用。为了在缓存中执行 SELECT 查询, NCache 利用 ExecuteReader;ExecuteScalar 函数用于执行查询并从结果数据集中检索第一行的第一列,而不考虑任何额外的列或行。
要使 NCache SQL 查询正常运行,必须在所有正在搜索的对象上建立索引。这可以通过两种方法实现:配置缓存或使用具有 “Custom Attributes” 的代码来注释对象字段。将对象添加到缓存时,此方法会自动在指定字段上创建索引。
代码片段
1 String cacheName = "demoCache"; 2 // Connect to the cache and return a cache handle 3 Cache cache = CacheManager.getCache(cacheName); 4 // Adds all the products to the cache. This automatically creates indexes on various 5 // attributes of Product object by using "Custom Attributes". 6 addSampleData(cache); 7 // $VALUE$ keyword means the entire object instead of individual attributes that are also possible 8 String sql = "SELECT $VALUE$ FROM com.alachisoft.ncache.samples.Product WHERE category IN (?, ?) AND price < ?"; 9 QueryCommand sqlCommand = new QueryCommand(sql); 10 List<String> catParamList = new ArrayList<>(Arrays.asList(("Electronics"), ("Stationery"))); 11 sqlCommand.getParameters().put("category", catParamList); 12 sqlCommand.getParameters().put("price", 2000); 13 14 // ExecuteReader returns ICacheReader with the query resultset 15 CacheReader resultSet = cache.getSearchService().executeReader(sqlCommand); 16 List<Product> fetchedProducts = new ArrayList<>(); 17 if (resultSet.getFieldCount() > 0) { 18 while (resultSet.read()) { 19 // getValue() with $VALUE$ keyword returns the entire object instead of just one column 20 fetchedProducts.add(resultSet.getValue("$VALUE$", Product.class)); 21 } 22 } 23 printProducts(fetchedProducts); 24
利用 SQL 中 NCache 通过关注对象属性和标签,而不是仅仅依赖键来对缓存数据执行查询。
在此示例中,我们使用 “Custom Attributes” 在 Product 对象上生成索引。
了解有关 SQL 查询的更多信息 NCache 在 Java 中使用 SQL 查询缓存中的数据
Read-Thru 和 Write-Thru
利用数据源提供商的功能 NCache 将其定位为微服务架构中数据访问的主要接口。当微服务需要数据时,它应该首先查询缓存。如果存在数据,则缓存会直接提供该数据。否则,缓存将采用直通处理程序代表客户端从数据存储中获取数据,对其进行缓存,然后将其提供给微服务。
以类似的方式,对于写入操作(例如 Add、Update、Delete),微服务可以对缓存执行这些操作。然后,缓存使用直通处理程序自动对数据存储执行相应的写入操作。
此外,您还可以选择强制缓存直接从数据存储中获取数据,而不管缓存中是否存在可能过时的版本。当微服务需要最新信息时,此功能至关重要,并补充了前面提到的缓存一致性策略。
数据源提供程序功能的集成不仅可以简化您的应用程序代码,而且当与 NCache的数据库同步功能,确保缓存始终使用新数据进行更新以进行处理。
ReadThruProvider
为了实现 Read-Through 缓存,必须在 Java 中创建 ReadThruProvider 接口的实现
以下是开始在微服务中实施 Read-Thru 的代码片段:
1 ReadThruOptions readThruOptions = new ReadThruOptions(ReadMode.ReadThru, _readThruProviderName); 2 product = _cache.get(_productId, readThruOptions, Product.class); 3
在此处阅读有关 Read-Thru 实施的更多信息:Read-Through Provider 配置和实施
WriteThruProvider 中:
要实现 Write-Through 缓存,必须在 Java 中创建 WriteThruProvider 接口的实现
开始在微服务中实施 Write-Thru 的代码片段:
1 _product = new Product(); 2 WriteThruOptions writeThruOptions = new WriteThruOptions(WriteMode.WriteThru, _writeThruProviderName) 3 CacheItem cacheItem= new CacheItem(_customer) 4 _cache.insert(_product.getProductID(), cacheItem, writeThruOptions); 5
总结
微服务设计为自治的,支持从其他微服务独立开发、测试和部署。虽然微服务在可扩展性和快速开发周期方面提供了优势,但应用程序堆栈的某些组件可能会带来挑战。其中一个挑战是使用关系数据库,它可能不支持必要的横向扩展来处理不断增长的负载。这就是像这样的分布式缓存解决方案的地方 NCache 变得有价值。
我们看到了各种即用型功能,如发布/订阅消息传递、数据缓存、SQL 查询、Read-Thru 和 Write-Thru,以及 Hibernate 提供的二级 Java 缓存技术 NCache 简化和精简数据缓存与您的微服务应用程序的集成,使其成为一种轻松自然的扩展。