世上无难事,只要肯放弃
前言
菜鸡来记录一下学习分布式缓存的过程。
说起这个缓存我立马就想到了当初学习计算机组成原理的时候学过的什么寄存器,高速缓冲,内存,磁盘,一级缓存,二级缓存,三级缓存之类的东西。我也了解过,一个程序做数据库的缓存也有一级缓存,二级缓存的概念。那么这是不是跟计算机组成原理学习的缓存很相似呢?根据我浅显的认知就大概提一嘴。
- CPU访问内存比较慢,所以需要高速缓存,让CPU访问高速缓存,而高速缓存与内存进行数据交换。
- 一个程序要访问数据库,其中与数据库建立连接,查询数据这一过程比较耗时间,所以把第一次查询到的数据进行缓存,下一次查询的时候就直接从缓存中拿到数据,不走数据库,这样能加快程序的速度。
在搜索如何做缓存的时候,有一篇文章说Ehcache无法实现分布式缓存,就算手动实现分布式缓存,也无法保证数据的强一致性。而Hazelcast是一个很好的分布式缓存解决方案。既然都这么说了,那必须得用一下这个Hazelcast。虽然我是一个连任何缓存都没有用过的菜鸡,但是,世上无难事,只要肯放弃。
该项目代码存放在码云上:https://gitee.com/siumu/blog_code.git
准备工作
首先呢,是要先准备数据库。建库、建表、建字段、建数据太麻烦,我就偷个懒,用V部落的sql文件直接导入,用它的user表,该数据库也会和该博客的代码一起上传到码云。这张表长这个样子:
接下来就是创建应用了。既然是分布式缓存,那肯定要创建多个应用来测试,所以我就创建一个maven多模块的项目,如何创建一个多模块项目,网上有很多教程,很简单,我就不写一遍了,写起来挺费劲的。如果是第一次创建多模块项目,可能会出现很多问题,慢慢解决就好了,学习就要有耐心。
我创建的项目目录结构如下:
- hazelcast_demo是父工程
- hazelcast-model是子模块,主要是创建实体类
- hazelcast-web是一个web应用,依赖hazelcast-model
- hazelcast-web-copy是对hazelcast-web应用的一个复制
这样就能启动两个web应用了。
hazelcast_demo作为父工程,没有src,就只有一个pom文件,项目需要的依赖在这里定义,它的pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>hazelcast-model</module>
<module>hazelcast-web</module>
<module>hazelcast-web-copy</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiumu</groupId>
<artifactId>hazelcast_demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hazelcast_demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.47</mysql.version>
<lombok.version>1.18.12</lombok.version>
<springboot.version>2.3.1.RELEASE</springboot.version>
<hazelcast.version>4.0</hazelcast.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
<version>${springboot.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.hazelcast/hazelcast -->
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>${hazelcast.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.hazelcast/hazelcast-spring -->
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>${hazelcast.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
hazelcast-model作为子模块之一,主要是写实体类,我们先引入依赖,再创建实体类,它的POM文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hazelcast_demo</artifactId>
<groupId>com.xiumu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hazelcast-model</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>
然后是两个实体类,User类,和Role类,整个项目我只用到了User类,实现一个简单的查询全部,Role类暂时没用上。
@Data
@Entity
public class User implements Serializable {
@Id
private Long id; //主键
private String username; //用户名
private String password; //密码
private String nickname; //昵称
private boolean enabled; //启用或禁用
@Transient
private List<Role> roles; //角色,暂时不用
private String email; //邮箱
private String userface; //头像
private Timestamp regTime; //注册时间
}
@Data
@Entity
@Table(name = "roles")
public class Role {
@Id
private Long id; //主键
private String name; //角色名称
}
好了,我们的实体类模块已经创建完成了,该模块的具体目录结构如下:
hazelcast-web作为子模块之一,依赖hazelcast-model,所以它只需要创建respository、service、controller层就可以了。它的POM文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hazelcast_demo</artifactId>
<groupId>com.xiumu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hazelcast-web</artifactId>
<dependencies>
<dependency>
<groupId>com.xiumu</groupId>
<artifactId>hazelcast-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后是创建repository层:
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
}
再创建service层:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUser(){
return userRepository.findAll();
}
}
再创建controller层:
@RestController
@RequestMapping("/hazelcastweb")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/getAllUser")
public List<User> getAllUser(){
return userService.getAllUser();
}
}
然后是创建一个启动类:
@SpringBootApplication
@EntityScan("com.xiumu.hazelcastmodel.entity")
public class HazelcastWebApplication {
public static void main(String[] args) {
SpringApplication.run(HazelcastWebApplication.class,args);
}
}
最后是配置文件application.yml
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///vueblog?characterEncoding=utf8&useSSL=false
username: root
password: root
jpa:
hibernate:
ddl-auto: none
show-sql: true
这样一个应用就算是创建成功了,理论上是可以启动成功的。但是我没有试过,我是把所有的文件准备完之后才启动的。接下来就是引入hazelcast,我们找到它的jar包,这里面有一个hazelcast-default.yaml文件。如下图所示:
将它复制出来,并改个文件名,改成hazelcast.yaml,放到application.yml文件的旁边。我在网上找的文档都是复制旁边那个xml,但是既然这里有yaml,那肯定也没问题。这个文件很长,但是先不用管放在resources文件夹下就行了。然后怎么启用缓存呢?很简单,就是在启动类上加上注解@EnableCaching
,所以最后的启动类长这样:
@EnableCaching
@SpringBootApplication
@EntityScan("com.xiumu.hazelcastmodel.entity")
public class HazelcastWebApplication {
public static void main(String[] args) {
SpringApplication.run(HazelcastWebApplication.class,args);
}
}
然后在service层里把那个方法上加上注解@Cacheable
,缓存它的返回值。最后的service层是这样的
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable("allUser")
public List<User> getAllUser(){
return userRepository.findAll();
}
}
这样我们基本上就配置完了,关于那个hazelcast.yaml配置文件一点都不需要改动。该模块最后的目录结构就是这样子了:
接下来我们需要将这个应用复制一份,用两个应用来测试,hazelcast-web-copy就是对hazelcast-web的一个复制,它俩的配置文件,repository,service,controller,启动类几乎都一模一样,这是它的目录结构:
注意这个路径问题和启动类的名字,其它的注解,配置啥的一模一样,啊对了,还有注意它配置的端口号。它的端口号要改成8081,controller得请求路径也要改一改,区分一下两个请求。
开始试验
接下来就是对项目进行测试了,没啥说的,启动就行了。我们先启动hazelcast-web应用,先不说别的,这个日志就很显眼:
这个日志就说明了我们创建了一个节点,接下来我们再启动另外一个应用hazel-cast-copy,我们就会看到以下日志:
现在节点变成了两个,接下来我们进行测试一下。
测试之前我要安利一个插件叫RestfulToolkit。我们测试接口一般都是用postman,但是用postman还要打开客户端,就很麻烦,而这个工具是IDEA的一个插件,不用切换窗口,直接就在IDEA里就可以测试接口,如图所示:
我们请求第一个接口,可以看到,获得返回值,并且打印出来查询的sql语句。
接下来我们请求第二个接口,如图所示
我们看到,它初始化了dispatcherServlet,确实接收到了请求,但是并没有走数据库,说明我们这个分布式缓存还是成功的。
中期总结
我听说如果要是用redis做集群,还要安装redis,然后再做主从配置,哨兵什么的。但是试验了Hazelcast之后我们发现,它只需要引入jar包,没有主从配置,一个应用就是一个节点,自动集群。
我们打开它的配置文件hazelcast.yaml,找到下图这个地方:
注意,这个network
,一看就是网络的意思,那么它肯定就是配置我们这个集群的网络,接下来是port
,这不就是端口号的意思嘛,auto-increment,port-count,port
这三个配置意思就是,我从5701这个端口开始创建节点,如果5701端口被占用,那么就自动加1,使用5702端口,一直被占用就一直增加,直到我们端口号增加了100个还是被占用,那么就该抛出异常了。我们看这个日志就能看到第一个应用的缓存节点使用5701端口号,第二个应用的缓存节点使用5702端口号。
接下来再往下看,这里有join
,这不就是加入的意思嘛,那么这里肯定就是配置节点是怎么自动加入集群的。multicast
意思就是使用组播协议来加入集群,我也是第一次听说这个东西,用我浅薄的认知来说一下,就是说节点启动的时候就向外广播,如果有其它节点回应,那节点就自动加入集群,如果没有那就它自己呗,等着别的新启动的节点给它广播。我们默认就是使用这个方式。我们找到这个日志:
这里有一个Creating MulticastJoiner
,这就是使用组播协议来进行集群。
跟multicast平行的是这个tcp-ip
,说明它也是集群的一种方式,只不过默认是关闭的,interface
这里是第一个节点启动的IP,也可以说它是个主节点吧,member-list
这不就是成员的IP地址嘛,是其他后续启动的节点的IP地址。其中的减号 " - ",表示这是一个数组,成员可以有很多IP地址,可以这么写
tcp-ip:
enabled: false
interface: 127.0.0.1
member-list:
- 127.0.0.1
- 127.0.0.2
- 127.0.0.3
- 127.0.0.4
- 127.0.0.5
好了,接下来的试验我们就需要用到tcp-ip这个配置了。
问题是这样的,我们这两个应用是在同一个机器上运行的,那么在不同的机器上如何使用hazelcast做分布式缓存呢?
不同机器上使用Hazelcast实现分布式缓存
准备工作
代码写起来倒是简单,准备工作倒是很费时间。
首先我们要准备两台机器,但是我只有一个电脑,所以就使用了虚拟机:centos7,网络呢使用桥接模式,这样有不同的ip地址,代表主机与虚拟机是两台机器。
虚拟机与主机的准备工作
- 首先,要测试虚拟机与主机之间可以ping通,不能的话就要解决这个问题。
- 第二,我没有远程数据库,用的是本地数据库,所以要让虚拟机可以访问主机的数据库,也就是创建一个用户允许虚拟机的IP可以远程访问,当然我偷了个懒,直接设置root用户允许任何IP进行远程登录。
- 第三,虚拟机要有运行java程序的环境,还要开放8081的端口号,可以让外界访问,因为我上传的应用程序就是占用的8081端口号。
- 查看端口开放
firewall-cmd --zone=public --list-port
- 开放8081端口
firewall-cmd --zone=public --add-port=8081/tcp --permanent
修改设置
首先我们要把组播模式关掉,打开tcp-ip模式,因为我还没摸索出来组播模式怎么配置不同机器之间的集群,然后配置我们的主机IP地址,和虚拟机IP地址,千万不要写127.0.0.1。具体修改后的配置如下
也不要忘了修改数据库的连接信息,要写上主机的IP地址,就像下图这样:
打包上传
将hazelcast-web-copy打包,上传至虚拟机。我将它打包成jar。
开始测试
接下来就可以测试了,首先我们启动主机上的两个应用,然后我们就可以看到打印的日志,表示我们使用tcp-ip模式来集群,并且现在有两个节点了。
然后我们再启动虚拟机上的应用,使用java -jar 包名
命令就可以启动虚拟机上的这个项目启动之后,部分日志如图所示:
我们发现有三个节点了,那么它到底能不能实现分布式缓存呢?这就需要验证一番了,我们看这三个节点的日志就知道,现在还没有初始化dispatcherServlet
,我们先给本机的8081端口发送一次请求。
我们看打印日志就发现它初始化了dispatcherServlet
并走了一次数据库,打印出了sql语句。
接下来我们给虚拟机的应用发送一次请求,结果如下:
我们看到它收到了请求,返回了数据,但是并没有走数据库。那我们继续给主机的8080端口发送一次请求。同样可以看到日志,它并没有走数据库。这就证明了我们的分布式缓存是成功的。
总结
通过这次试验,我们发现Hazelcast实现分布式缓存还是很简单的,只不过是我比较菜,整了两三天才摸索出来。说起缓存我们总是提起redis,ehcache,memcache,但是这个Hazelcast倒是很少有人提及,所以网上相关的博客很少,我遇到的问题有时候都搜索不到,甚至把网上的方法试了个遍都不行。可能是我还不是很会搜索问题吧。
有的博客写的就很神奇,最让我记忆深刻的就是要让我在启动类上加上@EnableHazelcastCaching这个注解,我完全都搜不到这个注解在哪,我现在想搜这个博客也搜索不到了,当然也有可能这是人家自定义的注解,手写了一个spring-boot-starter-hazelcast包。有的说让我设置store-type的,或者把实体类序列化的,等等各种情况,到最后我还是看到一个英文博客里使用了hazelcast-spring这个Jar包,我把它加上就好了。
刚开始搜这个Hazelcast如何使用的时候,大家的博客都说这个很简单,就引入一个hazelcast就可以使用,我深信不疑,因为确实看到了日志显示有集群,有三个节点,可是,访问任何一个节点都要走一次数据库,我还以为是我的使用有什么问题,毕竟没有使用过别的缓存框架,不是很会用。后来做成这个demo之后我又搜索了一下关于它的博客,发现确实是我眼瞎,有的博客也引入了两个jar包,而且是用gradle构建的项目,不知道为啥我居然没有看到它引入了两个jar包。可能是我意识里被这个只需要引入一个jar给洗脑了,下意识给忽略了。
好了,闲聊到此,总的来说,对于该工具更加细节的使用还是得去实践中学习。