文档结构  
翻译进度:已翻译     翻译赏金:0 元     ¥ 我要打赏

之前我写了一篇文章关于 使用 Hazelcast、Hibernate 和 Spring Boot 实现 JPA 缓存,我通过一个示例描述了如何使用 Hazelcast 作为 Hibernate 的二级缓存。该例子一个最大的缺点就是只能缓存的键只能是主键。而我们经常需要将 JPA 的查询结果缓存到内存中,但之前的这种方式并不能完全解决问题,即使它们所匹配的查询可以使用已经缓存的实体标准。

在这篇文章中,我将给你介绍一个基于 Hazelcast 分布式查询的聪明解决办法。

第 1 段(可获 1.2 积分)

Image title

Spring Boot 拥有一个内建的 Hazelcast 自动配置,前提是相关的库存在于应用的类路径中,并且声明了 @BeanConfig

@Bean
Config config() {
 Config c = new Config();
 c.setInstanceName("cache-1");
 c.getGroupConfig().setName("dev").setPassword("dev-pass");
 ManagementCenterConfig mcc = new ManagementCenterConfig().setUrl("http://192.168.99.100:38080/mancenter").setEnabled(true);
 c.setManagementCenterConfig(mcc);
 SerializerConfig sc = new SerializerConfig().setTypeClass(Employee.class).setClass(EmployeeSerializer.class);
 c.getSerializationConfig().addSerializerConfig(sc);
 return c;
}
第 2 段(可获 0.28 积分)

在上面的代码片段汇总,我们声明了一个到 Hazelcast 管理中心的集群名称和密码凭据以及连接参数,同时还包括实体的序列化配置。这个实体对象相当简单,包含一个 @Id 和两个用于搜索的字段 personId 和 company。

@Entity
public class Employee implements Serializable {

 private static final long serialVersionUID = 3214253910554454648 L;

 @Id
 @GeneratedValue
 private Integer id;
 private Integer personId;
 private String company;

 public Integer getId() {
  return id;
 }

 public void setId(Integer id) {
  this.id = id;
 }

 public Integer getPersonId() {
  return personId;
 }

 public void setPersonId(Integer personId) {
  this.personId = personId;
 }

 public String getCompany() {
  return company;
 }

 public void setCompany(String company) {
  this.company = company;
 }

}
第 3 段(可获 0.45 积分)

每个实体对象都需要声明序列化器,用来定义对象如何插入缓存以及从缓存中读取。Hazelcast 库内建了一些默认的序列化器,但在这个例子中我自己实现了一个,基于 StreamSerializer 和 ObjectDataInput。

public class EmployeeSerializer implements StreamSerializer < Employee > {

 @Override
 public int getTypeId() {
  return 1;
 }

 @Override
 public void write(ObjectDataOutput out, Employee employee) throws IOException {
  out.writeInt(employee.getId());
  out.writeInt(employee.getPersonId());
  out.writeUTF(employee.getCompany());
 }

 @Override
 public Employee read(ObjectDataInput in ) throws IOException {
  Employee e = new Employee();
  e.setId( in .readInt());
  e.setPersonId( in .readInt());
  e.setCompany( in .readUTF());
  return e;
 }

 @Override
 public void destroy() {}

}
第 4 段(可获 0.54 积分)

这里还有一个 DAO 接口用于和数据库交互。该接口扩展自 Spring Data 的 CrudRepository 接口,同时包含两个搜索方法。

public interface EmployeeRepository extends CrudRepository<Employee, Integer> {

    public Employee findByPersonId(Integer personId);
    public List<Employee> findByCompany(String company);

}

在这个例子中,Hazelcast 实例被嵌入到应用中,当我们启动 Spring Boot 应用时,我们必须为虚拟机提供 -DPORT 参数,用于输出服务的 REST API。Hazelcast 自动检测到其他成员实例,并自动的对给端口加1.这里是 REST @Controller 类以及输出的 API:

第 5 段(可获 0.96 积分)

@Service 被注入到 EmployeeController。在 EmployeeService 中有一个简单的实现在 Hazelcast 缓存实例和 Spring Data DAO @Repository 间的切换。每次执行 find 方法时,我们尝试在缓存中读取数据,如果不存在就从数据库中读取,并将读取到的数据写到缓存中。

@Service
public class EmployeeService {

 private Logger logger = Logger.getLogger(EmployeeService.class.getName());

 @Autowired
 EmployeeRepository repository;
 @Autowired
 HazelcastInstance instance;

 IMap < Integer, Employee > map;

 @PostConstruct
 public void init() {
  map = instance.getMap("employee");
  map.addIndex("company", true);
  logger.info("Employees cache: " + map.size());
 }

 @SuppressWarnings("rawtypes")
 public Employee findByPersonId(Integer personId) {
  Predicate predicate = Predicates.equal("personId", personId);
  logger.info("Employee cache find");
  Collection < Employee > ps = map.values(predicate);
  logger.info("Employee cached: " + ps);
  Optional < Employee > e = ps.stream().findFirst();
  if (e.isPresent())
   return e.get();
  logger.info("Employee cache find");
  Employee emp = repository.findByPersonId(personId);
  logger.info("Employee: " + emp);
  map.put(emp.getId(), emp);
  return emp;
 }

 @SuppressWarnings("rawtypes")
 public List < Employee > findByCompany(String company) {
  Predicate predicate = Predicates.equal("company", company);
  logger.info("Employees cache find");
  Collection < Employee > ps = map.values(predicate);
  logger.info("Employees cache size: " + ps.size());
  if (ps.size() > 0) {
   return ps.stream().collect(Collectors.toList());
  }
  logger.info("Employees find");
  List < Employee > e = repository.findByCompany(company);
  logger.info("Employees size: " + e.size());
  e.parallelStream().forEach(it -> {
   map.putIfAbsent(it.getId(), it);
  });
  return e;
 }

 public Employee findById(Integer id) {
  Employee e = map.get(id);
  if (e != null)
   return e;
  e = repository.findOne(id);
  map.put(id, e);
  return e;
 }

 public Employee add(Employee e) {
  e = repository.save(e);
  map.put(e.getId(), e);
  return e;
 }

}
第 6 段(可获 0.71 积分)

如果你对这个示例程序感兴趣,可以从 GitHub 获取完整的代码。在 person-service 模块中有一个我之前文章的例子(关于 Hazelcast 和 Hiberate 二级缓存的示例),在 employee-module 中存放的则是本文的例子。

测试

接下来让我们使用 -DPORT 的虚拟机参数来在不同的端口中启动三个雇员服务实例。在文章的第一个图中,端口是 2222、3333 和 4444.当启动第三个服务实例时,你将在应用日志中看到下面这段内容。这表明一个包含了三个成员的 Hazelcast 集群已经就绪。

第 7 段(可获 1.34 积分)
2017 - 05 - 09 23: 01: 48.127 INFO 16432-- - [ration.thread - 0] c.h.internal.cluster.ClusterService: [192.168 .1 .101]: 5703[dev][3.7 .7]

Members[3] {
 Member[192.168 .1 .101]: 5701 - 7 a8dbf3d - a488 - 4813 - a312 - 569 f0b9dc2ca
 Member[192.168 .1 .101]: 5702 - 494 fd1ac - 341 b - 451 c - b585 - 1 ad58a280fac
 Member[192.168 .1 .101]: 5703 - 9750 bd3c - 9 cf8 - 48 b8 - a01f - b14c915937c3 this
}

下面是来自 Hazelcast 管理中心中,运行了两个成员的截图(目前免费版的 Hazelcast 管理中心只支持两个成员)。

第 8 段(可获 0.33 积分)

Image title

接下来运行包含 MySQL 和 Hazelcast 管理中心的 Docker 容器:

docker run -d --name mysql -p 33306:3306 mysql
docker run -d --name hazelcast-mgmt -p 38080:8080 hazelcast/management-center:latest

现在你可以尝试在你的服务中调用端点 http://localhost:/employees/company/{company} 。当你在不同的服务中调用端点时,你应该可以看到数据已经在集群中缓存。我们会读取到其他服务写入到缓存的数据。多次尝试后,我的服务已经写入大约 10 万个对象到缓存中。两个 Hazelcast 成员之间的分布式是 50% 到 50%。

Image title

第 9 段(可获 1 积分)

总结

我们或许可以实现一个更智能的解决方案,但是我只是向你思路。我尝试使用 Spring Data Hazelcast 来解决,但是当我在 Spring Root 应用中使用它的时候碰到了问题。它包含一个 HazelcastRepository 接口,类似 Spring Data 的 CrudRepository ,但是是基于 Hazelcast 网格中的缓存实体,并且使用了 Spring Data 的 KeyValue 模块。此外,这个项目的文档并不完善,我之前曾经说过它不支持 Spring Boot ,因此我决定实现我自己的简单方案。

在我的本地环境中,正如本文前面所说,我插入了两百万条记录到雇员表中,在缓存上查询比在数据库上查询要快至少 10 倍。Hazelcast 数据网格不仅仅是一个二级缓存,更是介于你的应用和数据库之间的一个中间件。如果你的应用对性能以及大量数据查询非常在意,并且你拥有大量的内存,那么内存中的数据网格正是为你所准备的。

第 10 段(可获 2.21 积分)

文章评论