Chia sẻ kiến thức lập trình, kĩ năng mềm từ góc nhìn của một Engineer

Hazelcast Distributed Cache with Spring Boot

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.

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:

docker-compose up
các service đã chạy thành công trong docker

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

dashboard
member hazelcast
member hazelcast detail

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

thực hiện get list product lần đầu
log SQL khi thực hiện get list lần đầu

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

thực hiện get list lần 2
log SQL khi thực hiện get list lần 2

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

tạo product mới
log SQL sau khi tạo 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

get list lần 3 sau khi thêm mới product
log sql sau khi gọi api get list lần 3
Hazelcast dashboard overview

Các bạn để ý các mục được đánh dấu đỏ:

  • hzProducts : tên cache name mà ta đã quy ước ban đầu
  • cột Gets : số lần chúng ta thực hiện gọi lên Hazelcast để lấy về data
  • cộ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:

1 Comment

  1. mostbet

    Thank you very much for the information provided

© 2024 Cafeincode

Theme by Anders NorenUp ↑