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
- Crack Intellij IDEA Ultimate version 2022
- Distributed Lock with Hazelcast and Spring
- How to build Rate Limit with Hazelcast and Spring Boot
- Biết sử dụng git cherry-pick để làm việc hiệu quả hơn
- Git revert với Git reset hoạt động như thế nào?
- Git stash giúp bạn trở nên chuyên nghiệp như thế nào
- Câu chuyện phỏng vấn online mùa Covid
- Bạn không giỏi lắng nghe như bạn nghĩ đâu
- Nói sao để được chào đón, làm thế nào để được ghi nhận
- Series tìm hiểu System Design
- Series tìm hiểu Hazelcast
- Series tìm hiểu lập trình java
- Series crack Intellij IDEA
- Series tìm hiểu Docker
- Series tìm hiểu Git
- Series tìm hiểu Kafka
- Series tìm hiểu ElasticSearch
- Series tìm hiểu Linux
- Series phỏng vấn kĩ sư phần mềm
- Series review sách
Thank you very much for the information provided