Hazelcast là một Distributed Cache nhiều node rất phổ biến và mạnh mẽ, nó lưu các bản sao của các mảnh dữ liệu trên nhiều node. Khi một node bị lỗi thì dữ liệu trên node đó sẽ được khôi phục lại từ bản backup và cụm Hazelcast vẫn hoạt động bình thường mà không bị downtime.
Trước khi đi vào chi tiết, chúng ta cần cài đặt môi trường, ở bài viết này mình sử dụng Docker Desktop trên windows 10, về cơ bản nó tiện lợi cho mình vì không cần phải tải bản cài hazelcast rồi chạy như những bài hướng dẫn trước nữa.
Mục Lục
Dựng cấu trúc dự án và thiết lập tài nguyên
Khởi tạo một dự án spring boot bằng spring initializr, trong file pom.xml nhúng thư viện của Hazelcast vào, mình sử dụng version mới nhất thời điểm hiện tại là 5.2.1
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>5.2.1</version>
</dependency>
Tạo file docker-compose chứa các service
các bạn cần tạo một file đặt tên là docker-compose.yml
, sử dụng nội dung bên dưới để cấu hình các service:
version: "3.8"
services:
hazelcast:
container_name: cafeincode-hazelcast
image: hazelcast/hazelcast:5.2.1
ports:
- "5701:5701"
management-center:
container_name: cafeincode-hazelcast-management
image: hazelcast/management-center:5.2.1
ports:
- "8080:8080"
environment:
- MC_DEFAULT_CLUSTER=dev
- MC_DEFAULT_CLUSTER_MEMBERS=hazelcast
mysql:
container_name: cafeincode-mysql
image: mysql:5.7
ports:
- 3307:3306
environment:
MYSQL_DATABASE: "cafeincode_schema"
MYSQL_ROOT_PASSWORD: "123456789"
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
Các service gồm có Mysql, Hazelcast server, Hazelcast management để xem giao diện quản lý các cluster Hazelcast dưới dạng UI.
Mở terminal và chạy lệnh docker-compose up
để chạy file docker-compose.yml
, kết quả sau khi chạy xem hình bên dưới:


Ta truy cập vào http://localhost:8080
để vào hazelcast-management để xem trình quản lý cluster của hazelcast



Thêm Script để tạo bảng và Insert dữ liệu vào bảng
CREATE TABLE `product` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT 'id giao dịch',
`code` varchar(20) DEFAULT NULL,
`price` decimal(10,0) DEFAULT NULL,
`created_date` datetime DEFAULT NULL,
`last_updated` datetime DEFAULT NULL,
`created_by` varchar(20) DEFAULT 'hungtv27',
`status` int(11) DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('AnSbdWLXtMS2', '1bMqEJjh', '1000', '2023-01-11 00:44:01', '2023-01-11 00:44:01', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('AALd2lkieVx7', '2a3v81zq', '1000', '2023-01-11 00:47:50', '2023-01-11 00:47:50', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('2yXgjpItmx05', '1QVt8H2C', '1000', '2023-01-11 00:47:52', '2023-01-11 00:47:52', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('dkDViYonl3Sy', 'N32pmZxh', '1000', '2023-01-11 00:47:53', '2023-01-11 00:47:53', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('tryiYMDXC3hd', 'POgUFkHq', '1000', '2023-01-11 00:47:54', '2023-01-11 00:47:54', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('LJ1UYtBX1jZ7', '5XjSEgcs', '1000', '2023-01-11 00:47:55', '2023-01-11 00:47:55', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('I17rw3i5J0F4', 'ACKjzgXS', '1000', '2023-01-11 00:47:56', '2023-01-11 00:47:56', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('Ayq7gaw1b64q', '1qXRSKPp', '1000', '2023-01-11 00:47:57', '2023-01-11 00:47:57', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('qpqWobUa404m', 'cJLOc84Z', '1000', '2023-01-11 00:47:58', '2023-01-11 00:47:58', 'hungtv27', '1');
INSERT INTO `product` (`name`, `code`, `price`, `created_date`, `last_updated`, `created_by`, `status`) VALUES ('V17fqI0Y736Y', 'zm0YUjzR', '1000', '2023-01-11 00:47:59', '2023-01-11 00:47:59', 'hungtv27', '1');
Xây dựng cấu trúc project

package com.cafeincode.hazelcast.config;
import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.spring.cache.HazelcastCacheManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.PostConstruct;
/**
* @author hungtv27
*/
@Configuration
@EnableTransactionManagement
@EnableCaching
public class HazelcastCacheManagerConfig extends CachingConfigurerSupport {
@Value("#{'${hazelcast.cafeincode.address}'.split(',')}")
protected String[] address;
@Value("${hazelcast.cafeincode.cluster_name}")
private String clusterName;
@Bean(name = "hazelcastClient")
@PostConstruct
public HazelcastInstance hazelcastInstance() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.setClusterName(clusterName);
clientConfig.getNetworkConfig().addAddress(address);
clientConfig.getNetworkConfig().setConnectionTimeout(50000);
return HazelcastClient.newHazelcastClient(clientConfig);
}
@Override
@Bean
public CacheManager cacheManager() {
return new HazelcastCacheManager(hazelcastInstance());
}
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(".");
sb.append(method.getName());
sb.append(":params:");
for (Object obj : params) {
sb.append(String.format("[%s]", obj));
}
return sb.toString();
};
}
}
package com.cafeincode.hazelcast.jpa;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Entity
@Table(name = "product")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Product implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "code")
private String code;
@Column(name = "price")
private BigDecimal price;
@Column(name = "status")
private Integer status;
@Column(name = "created_by")
private String createdBy;
@Column(name = "created_date")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Column(name = "last_updated")
@Temporal(TemporalType.TIMESTAMP)
private Date lastUpdated;
@PrePersist
public void prePersist() {
if (this.createdDate == null) {
this.createdDate = new Date();
}
}
}
package com.cafeincode.hazelcast.repository;
import com.cafeincode.hazelcast.jpa.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository(value = "productRepo")
public interface ProductRepo extends JpaRepository<Product, Long> {
List<Product> getProductByStatus(Integer status);
}
package com.cafeincode.hazelcast.service;
import com.cafeincode.hazelcast.jpa.Product;
import java.util.List;
public interface ProductService {
List<Product> getProductByStatus(Integer status);
Product createProduct();
void deleteProduct(Long id);
}
package com.cafeincode.hazelcast.service.impl;
import com.cafeincode.hazelcast.constant.Constant;
import com.cafeincode.hazelcast.jpa.Product;
import com.cafeincode.hazelcast.repository.ProductRepo;
import com.cafeincode.hazelcast.service.ProductService;
import net.bytebuddy.utility.RandomString;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Service(value = "productService")
public class ProductServiceImpl implements ProductService {
private final String cacheName = "hzProducts";
@Resource
private ProductRepo productRepo;
@Override
@Cacheable(value = cacheName)
@Transactional(readOnly = true)
public List<Product> getProductByStatus(Integer status) {
return productRepo.getProductByStatus(status);
}
@Override
@CacheEvict(value = cacheName, allEntries = true)
@Transactional(rollbackFor = Exception.class)
public Product createProduct() {
Product product = Product.builder()
.name(RandomString.make(12))
.code(RandomString.make(8))
.createdBy("hungtv27")
.price(new BigDecimal(1000))
.createdDate(new Date())
.lastUpdated(new Date())
.status(Constant.ACTIVE)
.build();
return productRepo.save(product);
}
@Override
@CacheEvict(value = cacheName, allEntries = true)
@Transactional(rollbackFor = Exception.class)
public void deleteProduct(Long id) {
Optional<Product> product = productRepo.findById(id);
product.ifPresent(value -> productRepo.deleteById(value.getId()));
}
}
spring.datasource.url=jdbc:mysql://localhost:3307/cafeincode_schema?useEncoding=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useLegacyDatetimeCode=false&serverTimezone=Asia/Ho_Chi_Minh&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456789
spring.jpa.properties.hibernate.format_sql=true
spring.jackson.time-zone=Asia/Ho_Chi_Minh
spring.jackson.date-format=dd-MM-yyyy HH:mm:ss
spring.datasource.hikari.maxLifeTime=600000
spring.jpa.show-sql=true
spring.main.allow-circular-references=true
hazelcast.cafeincode.address=localhost:5701
hazelcast.cafeincode.cluster_name=dev
server.port=8888
Việc thiết lập cấu hình và xử lý code tới đây là xong, bây giờ chúng ta chạy ứng dụng lên và dùng postman để test dữ liệu đổ vào Hazelcast như thế nào
Truy cập đường dẫn http://localhost:8888/products
từ postman để gọi api lấy danh sách product, lúc này dữ liệu sẽ được lấy từ database lên lần đầu, sau đó data được đẩy vào cache của hazelcast có name là hzProducts


Kiểm tra log sql ở console thì ta thấy có log được in ra, chứng tỏ dữ liệu đã được lấy từ DB rồi
Tiếp theo chúng ta thực hiện get list lần thứ 2, để chứng mình dữ liệu được lấy từ Hazelcast


Lúc này dữ liệu không còn lấy từ DB nữa mà đã lấy từ Hazelcast, tiếp tục thực hiện một lần nữa với api create product


Sau khi tạo product mới thì lúc này dữ liệu trong cache đã bị clear, do trong code ở trên chúng ta thiết lập CacheEvict ở hàm tạo.
@CacheEvict(value = cacheName, allEntries = true)
Tiếp theo, chúng ta kiểm chứng lại bằng cách gọi api danh sách product và tra log sql ở console



Các bạn để ý các mục được đánh dấu đỏ:
hzProducts
: tên cache name mà ta đã quy ước ban đầucột Gets
: số lần chúng ta thực hiện gọi lên Hazelcast để lấy về datacột Sets
: số lần chúng ta cập nhật data mới vào Hazelcast
Các bạn có thể download source tại đây: cafeincode/hz-example
Xem thêm các bài hướng dẫn liên quan dưới đây:
- What is Hazelcast?
- Cài đặt Hazelcast trên server Centos 7
- Build hệ thống Pub Sub dùng Hazelcast và Spring boot
- Elasticsearch là gì mà bá đạo đến vậy? [Phần 1]
- Elasticsearch và Kibana dựng bằng Docker
- Series tìm hiểu lập trình java
- Cùng nhau tìm hiểu Docker
- Active Jrebel để code trong IntellIJ IDEA
- Lập trình viên lúc rảnh rỗi thì nên làm gì?
- Bạn không giỏi lắng nghe như bạn nghĩ đâu
- Câu chuyện phỏng vấn online mùa Covid
- Nói sao để được chào đón, làm thế nào để được ghi nhận
- 13 Plugin không thể thiếu khi làm việc với IntellIJ IDEA
- Những plugins Intellij IDEA tốt nhất trong công việc
- Crack Intellij IDEA new versions 2021
- Crack IntellIJ để code như một senior
- Shortcut Intellij hữu ích để làm việc được hiệu quả hơn
- Build hệ thống Pub-Sub bằng Kafka+Spring boot (phần 3)
- Git stash giúp bạn trở nên chuyên nghiệp như thế nào?
- Git revert với git reset hoạt động như thế nào