首页 文章 精选 留言 我的

精选列表

搜索[SpringBoot],共4254篇文章
优秀的个人博客,低调大师

SpringBoot 项目优雅实现读写分离 | 京东云技术团队

一、读写分离介绍 当使用Spring Boot开发数据库应用时,读写分离是一种常见的优化策略。读写分离将读操作和写操作分别分配给不同的数据库实例,以提高系统的吞吐量和性能。 读写分离实现主要是通过动态数据源功能实现的,动态数据源是一种通过在运行时动态切换数据库连接的机制。它允许应用程序根据不同的条件或配置选择不同的数据源,以实现更灵活和可扩展的数据库访问。 二、实现读写分离-基础 1. 配置主数据库和从数据库的连接信息 # 主库配置 spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.master.username=master spring.datasource.master.password=123456 spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver # 从库配置 spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.slave.username=slave spring.datasource.slave.password=123456 spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver 2. 创建主数据库和从数据库的数据源配置类 通过不同的条件限制和配置文件前缀可以完成不同数据源的创建工作,不止是主从也可以是多个不同的数据库 主库数据源配置 @Configuration @ConditionalOnProperty("spring.datasource.master.jdbc-url") public class MasterDataSourceConfiguration { @Bean("masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } } 从库数据源配置 @Configuration @ConditionalOnProperty("spring.datasource.slave.jdbc-url") public class SlaveDataSourceConfiguration { @Bean("slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } } 3. 创建主从数据源枚举 public enum DataSourceTypeEnum { /** * 主库 */ MASTER, /** * 从库 */ SLAVE, ; } 4. 创建动态路由数据源 这儿做了一个开关,可以控制读写分离的开启和关闭工作,可以讲操作全部切换到主库进行。然后根据上下文中的数据源类型来返回不同的数据源类型枚举 @Slf4j public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Value("${DB_RW_SEPARATE_SWITCH:false}") private boolean dbRwSeparateSwitch; @Override protected Object determineCurrentLookupKey() { if(dbRwSeparateSwitch && DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())) { log.info("DynamicRoutingDataSource 切换数据源到从库"); return DataSourceTypeEnum.SLAVE; } log.info("DynamicRoutingDataSource 切换数据源到主库"); // 根据需要指定当前使用的数据源,这里可以使用ThreadLocal或其他方式来决定使用主库还是从库 return DataSourceTypeEnum.MASTER; } } 5. 创建动态数据源配置类 将主数据库和从数据库的数据源添加到动态数据源中,并可以通过枚举创建一个数据源 map,这样就可以通过上面的路由返回的枚举来切换数据源 @Configuration @ConditionalOnProperty("spring.datasource.master.jdbc-url") public class DynamicDataSourceConfiguration { @Bean("dataSource") @Primary public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceTypeEnum.MASTER, masterDataSource); targetDataSources.put(DataSourceTypeEnum.SLAVE, slaveDataSource); DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; } } 6. 创建DatasourceContextHolder类使用ThreadLocal存储当前线程的数据源类型 注意这儿有个潜在风险就是创建新的线程时会导致 ThreadLocal 中的数据无法正确读取,如果涉及到在开启新线程可以使用 TransmittableThreadLocal 来进行父子线程数据的同步,git 地址: https://github.com/alibaba/transmittable-thread-local public class DataSourceContextHolder { private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>(); public static void setDataSourceType(DataSourceTypeEnum dataSourceType) { contextHolder.set(dataSourceType); } public static DataSourceTypeEnum getDataSourceType() { return contextHolder.get(); } public static void clearDataSourceType() { contextHolder.remove(); } } 7. 创建自定义注解,用于标记主和从数据源 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MasterDataSource { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SlaveDataSource { } 8. 创建切面类,拦截数据库操作,并根据注解设置切换数据源参数 @Aspect @Component public class DataSourceAspect { @Before("@annotation(xxx.MasterDataSource)") public void setMasterDataSource(JoinPoint joinPoint) { DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER); } @Before("@annotation(xxx.SlaveDataSource)") public void setSlaveDataSource(JoinPoint joinPoint) { DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.SLAVE); } @After("@annotation(xxx.MasterDataSource) || @annotation(xxx.SlaveDataSource)") public void clearDataSource(JoinPoint joinPoint) { DataSourceContextHolder.clearDataSourceType(); } } 9. 在Service层的方法上使用自定义注解标记查询数据源 @Service public class TestService { @Autowired private TestDao testDao; @SlaveDataSource public Test test() { return testDao.queryByPrimaryKey(11L); } } 10. 排除掉数据源自动配置类 如果不排除自动配置类会导致初始化多个 dataSource 对象导致出现问题 SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 三、实现读写分离-进阶 1. 使用链接池,以Hikari为例 修改链接配置,加入链接池相关配置即可 # 主库配置 spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.master.username=master spring.datasource.master.password=123456 spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource spring.datasource.master.hikari.name=master spring.datasource.master.hikari.minimum-idle=5 spring.datasource.master.hikari.idle-timeout=30 spring.datasource.master.hikari.maximum-pool-size=10 spring.datasource.master.hikari.auto-commit=true spring.datasource.master.hikari.pool-name=DatebookHikariCP spring.datasource.master.hikari.max-lifetime=1800000 spring.datasource.master.hikari.connection-timeout=30000 spring.datasource.master.hikari.connection-test-query=SELECT 1 # 从库配置 spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.slave.username=root spring.datasource.slave.password=123456 spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource spring.datasource.slave.hikari.name=master spring.datasource.slave.hikari.minimum-idle=5 spring.datasource.slave.hikari.idle-timeout=30 spring.datasource.slave.hikari.maximum-pool-size=10 spring.datasource.slave.hikari.auto-commit=true spring.datasource.slave.hikari.pool-name=DatebookHikariCP spring.datasource.slave.hikari.max-lifetime=1800000 spring.datasource.slave.hikari.connection-timeout=30000 spring.datasource.slave.hikari.connection-test-query=SELECT 1 2. 集成 mybatis 并在写入时强制切换到主库 不需要做任何配置,正常集成 mybatis 即可使用读写分离功能 可以通过 mybatis 的拦截器在写入操作时强制切换到主库 @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), }) @Component public class WriteInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取 SQL 类型 DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDataSourceType(); if(DataSourceTypeEnum.SLAVE.equals(dataSourceType)) { DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER); } try { // 执行 SQL return invocation.proceed(); } finally { // 恢复数据源 考虑到写入后可能会反查,后续都走主库 // DataSourceContextHolder.setDataSourceType(dataSourceType); } } }  作者:京东健康苏曼 来源:京东云开发者社区 转发请注明来源

优秀的个人博客,低调大师

mall4j 商城系统,Springboot3 升级

现在jdk17和spring boot的第三方依赖已经趋于成熟,所以mall4j也一把梭哈做了升级嗷。🎉🎉🎉 🎉🎉🎉本次更新重点: 系统由jdk8最低要求升级到jdk17 spring boot由2.7.x升级到3.0.x dockerfiler使用openjdk17 还有一些其他的更新: 返回值类型替换为ServerResponseEntity 升级mybatis-plus到3.5.3.1 mysql驱动替换 spring-cloud-commons升级到4.0.1 redisson升级到3.19.3 hutool升级到5.8.15 一些其他小改动,具体看提交记录 对于不知道咋搭建开发环境的同学还增加了开发环境搭建的视频噢。 视频链接:https://www.bilibili.com/video/BV1eW4y1V7c1 这代码有没有文档呀? 当然有啦,你已经下载了,在doc这个文件夹上,实在不知道,我就给链接出来咯: gitee:https://gitee.com/gz-yami/mall4j/tree/master/doc

优秀的个人博客,低调大师

手把手教你用 Jenkins 自动部署 SpringBoot

@[toc] 1. 什么是 CI/CD CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。 CI/CD 的核心概念可以总结为三点: 持续集成 持续交付 持续部署 CI/CD 主要针对在集成新代码时所引发的问题(俗称"集成地狱")。 为什么会有集成地狱这个“雅称”呢?大家想想我们一个项目部署的过程,拉取代码-&gt;构建-&gt;测试-&gt;打包-&gt;部署,如果我们经常需要部署项目,特别是在微服务时代,服务特别多的情况下,不停的测试打包部署,那估计得有个人一整天专门做这事了,而这事又是繁琐的重复无意义的。 具体而言,CI/CD 可让持续自动化和持续监控贯穿于应用的整个生命周期(从集成和测试阶段,到交付和部署),这些关联的事务通常被统称为"CI/CD 管道",由开发和运维团队以敏捷方式协同支持。 1.1 CI(Continuous Integration) CI/CD 中的"CI"始终指持续集成,它属于开发人员的自动化流程。成功的 CI 意味着应用代码的新更改会定期构建、测试并合并到代码仓库中,该解决方案可以解决在一次开发中有太多应用分支,从而导致相互冲突的问题。 1.2 CD(Continuous Delivery/Continuous Deployment) CI/CD 中的"CD"指的是持续交付和/或持续部署,这些相关概念有时会交叉使用。两者都事关管道后续阶段的自动化,但它们有时也会单独使用,用于说明自动化程度。 持续交付(Continuous Delivery)通常是指开发人员对应用的更改会自动进行错误测试并上传到代码仓库(如 GitHub、GitLab 等),然后由运维团队将其部署到实时生产环境中。这旨在解决开发和运维团队之间可见性及沟通较差的问题。因此,持续交付的目的就是确保尽可能减少部署新代码时所需的工作量。 持续部署(Continuous Deployment)指的是自动将开发人员的更改从代码仓库发布到生产环境,以供客户使用。通过一套全自动化的流程,来解决手动测试、编译、打包等操作。持续部署以持续交付的优势为根基,实现了管道后续阶段的自动化。 2. 什么是 Jenkins 前面说的 CI/CD 算是一种思想,思想要落地,就需要对应的工具。 Jenkins 是一款开源的 CI/CD 软件,可以算是 CI/CD 软件领导者,它提供了超过 1000 个插件来支持构建、部署、自动化,基本上能够满足任何项目的需要。 整体来说,Jenkins 有如下六大特点: 持续集成和持续交付 作为一个可扩展的自动化服务器,Jenkins 可以用作简单的 CI 服务器,或者变成任何项目的持续交付中心。 简易安装 Jenkins 是一个基于 Java 的独立程序,可以立即运行,包含 Windows、Mac OS X 和其他类 Unix 操作系统。 配置简单 Jenkins 可以通过其网页界面轻松设置和配置,其中包括即时错误检查和内置帮助。 插件 通过更新中心中的 1000 多个插件,Jenkins 集成了持续集成和持续交付工具链中几乎所有的工具。 扩展 Jenkins 可以通过其插件架构进行扩展,从而为 Jenkins 可以做的事提供几乎无限的可能性。 分布式 Jenkins 可以轻松地在多台机器上分配工作,帮助更快速地跨多个平台推动构建、测试和部署。 其实 Jenkins 有很多好玩的用法,今天我还是想先通过一个简单的案例,先来和大家捋一捋如何使用 Jenkins 来实现一个 Spring Boot 项目的自动发布部署,这样大家对 Jenkins 现有一个直观的认知,各种其他使用细节松哥在以后的文章中再来和大家细聊。 3. 准备工作 3.1 整体规划 我们先来通过如下一张图片来看下 Jenkins 在整个流程中扮演的角色: 结合第一二小节的介绍,这张图应该很好理解。 3.2 准备代码 提前准备好测试代码,并上传到代码仓库中。为了更加逼真一些,小伙伴们可以将这个代码仓库设置为私有的,这样将来可以检验 Jenkins 中的配置是否正确。 考虑到 GitHub 网络有时候不稳定,我这里使用了 Gitee,一个很简单的 Spring Boot 工程,里边有一个 hello 接口,仅此而已。 我的代码仓库地址(私有仓库): https://gitee.com/lenve/jenkins_demo.git 3.3 准备服务器 理论上,我们需要一台服务器用来跑 Jenkins,还需要一台服务器作为我的应用服务器,但是我手头没有多余的服务器,所以我就将 Jenkins 和我的 Spring Boot 项目部署到一台服务器上,在接下来的文章中我会和大家说明每个配置是针对 Jenkins 的还是针对 Spring Boot 的。 另外,有的小伙伴可能是在虚拟机上做实验,因为将来我们的代码提交到 Gitee 之后,Gitee 会通过一个 POST 请求将这个事件告知 Jenkins,进而触发 Jenkins 的构建操作。所以这就要求 Gitee 能够访问到你的 Jenkins 服务器,所以如果你的 Jenkins 刚好搭建在服务器上,这事就很容易了,但如果是搭建在虚拟机里,就得通过花生壳之类的内网穿透工具来辅助你的工作了,比较麻烦,而且花生壳网速也慢。 不过小伙伴们不必担心,如果你在虚拟上搭建的 Jenkins,并且不愿意折腾花生壳,那么也可以通过手动构建/定时构建的方式去完成项目构建的。 4. 搭建 Jenkins 为了省事,我决定用 Docker 搭建 Jenkins,一行命令搞定。 为了操作方便,我们将 Jenkins 的工作目录映射到我的宿主机中来,因此首先在宿主机中准备一个数据目录(不是必须): # 创建 jenkins 目录 mkdir /data/jenkins_home/ # 修改目录的所有者,以便于 Jenkins 容器能够操作该目录 chown -R 1000:1000 /data/jenkins_home/ 接下来创建并启动 Jenkins 容器,同时挂载数据卷: docker run -d --name jenkins -p 8088:8080 -p 50000:50000 -v /data/jenkins_home:/var/jenkins_home jenkins/jenkins 由于 Jenkins 在运行的时候需要用到 maven,所以有的人会选择将 maven 目录也作为挂载点,但是我觉得没有必要,特别是对于初学者而言,这块很容易出错,不如将 maven 将来直接拷贝到 Jenkins 容器中,这样反而省事一些。 执行如上命令,安装成功之后,浏览器输入 http://localhost:8088 就可以访问了。 然后稍等片刻,就可以访问 Jenkins 了: 访问之前,首先需要解锁 Jenkins,解锁密码位置网页上列出来了,但是由于我们创建容器的时候设置了数据卷,所以,现在直接去宿主机的 /data/jenkins_home/secrets/initialAdminPassword 位置查看初始化密码,如下: 在网页中,输入密码然后继续。接下来会让我们选择需要的插件,第一次使用,安装推荐插件即可。 如果因为网络原因安装失败,可以点击重试按钮进行重试。 接下来创建一个新的用户,也可以不创建新的用户,直接使用 admin 即可: 再设置 Jenkins 访问地址: 这个页面有乱码,不过不影响,设置完成后,我们点击保存并完成按钮即可。接下来就可以进入到 Jenkins 中了。 整个过程执行完毕后,建议执行如下命令重启一下 Jenkins,因为有的插件需要重启之后才会生效。 docker restart jenkins 5. 安装插件 Jenkins 启动成功之后,接下来我们安装三个必要的插件: Maven Integration:Maven 构建工具 Publish Over SSH:整个工具,将来把 Jenkins 打包好的 jar 上传到应用服务器上。 Gitee:协助使用 Gitee 仓库。 安装步骤如下: 点击左边的系统管理,然后点击右边的插件管理,进行配置。 然后在可选插件中,搜索 Maven Integration 和 Publish Over SSH 以及 Gitee 三个插件: 搜索完成后,点击 Install without restart。 安装成功之后,重启 Jenkins。 建议执行 docker restart jenkins 去重启,点击网页上的重启,会卡很久,还是执行 docker 重启命令靠谱一些。 6. 配置 Jenkins 6.1 基本的环境配置 插件安装成功之后,接下来我们开启 Jenkins 的配置,在正式开始配置之前,先做一点准备工作。 这个需要我们提前准备好 Maven,由于 Jenkins 容器中已经包含一个 JDK 了,所以我们可以不用提前准备 JDK,只需要提前准备 Maven 即可。为了避免权限问题,我们可以直接将 Maven 上传到 jenkins 容器中,然后去配置即可。 如下将宿主机中的 maven 拷贝到 Jenkins 容器中: # 这个命令表示将宿主机中的 maven 目录拷贝到 jenkins 容器中的 /opt/ 目录下 docker cp maven jenkins:/opt/ 接下来就可以开始配置了,配置的位置如下图: 6.2 JDK 首先我们来配置 JDK,Jenkins 中默认安装了 JDK,我们只需要将其配置配出出来即可: 别名随意取,JAVA_HOME 则根据实际情况配置。 6.3 Maven Maven 就是我们刚刚上传到 docker 中期中的 Maven,配置一下位置即可,Jenkins 将来会自动从 Gitee 上将代码拉下来,然后就利用你这里配置的 Maven 进行构建: 名字随意取,MAVEN_HOME 则是前面刚刚上传到容器中的 MAVEN 目录。 6.4 Git 配置 Git,由于 Jenkins 容器中已经存在 git 了,所以这里不需要额外安装 git,默认即可。 所有都配置完成,点击保存按钮。 6.5 远程的凭证配置 接下来还需要我们配置两个远程登录凭证。 6.5.1 应用服务器信息 应用服务器,就是将来 Jenkins 将代码构建成 jar 包后,要上传的服务器的信息(地址、用户名以及密码)。 配置步骤如下,首先找到配置的位置: 往下拉找到 Publish Over SSH,然后点击新增,开始配置,Hostname 位置填写你服务器的域名或者 IP: 配置成功后,点击测试连接进行测试,确保连接是成功的。 > 有的小伙伴反馈这里用户名密码会导致 jar 包上传失败,要在应用服务器上生成 ssh 密钥对,然后将私钥配置给 Jenkins(这块大家结合自己情况来看,如果后面 jar 上传失败,可以回来改一下这里)。 6.5.2 Gitee 的信息 接下来我们配置 Gitee 的信息。 首先配置仓库的基本信息: 接下来配置 Gitee 的凭证,要根据这些凭证,才能从 Gitee 上拉取代码下来,点击 添加-&gt;Jenkins,添加凭证: 添加成功之后,就可以选择这个令牌了。 最后点击测试连接,确保可以连上 Gitee。 所有配置工作都做完了,接下来我们就可以开始构建一个项目了。 7. 开始一个项目的构建 首先我们新建一个任务 接下来我们选择构建一个 Maven 项目 点击确定之后,拉到源码管理位置,开始配置。 首先选择 Git,填入 Gitee 上的仓库地址,然后凭证就写 Gitee 的用户名/密码。 这里有一个需要注意的地方,就是默认的分支名称,GitHub 上现在默认的主分支名称是 main,Gitee 似乎还是 master,这个无所谓了,但是小伙伴们注意图片下面的分支,按你实际的情况填写。 这里也要添加凭证信息: 这里也要注意下,有小伙伴反馈 Gitee 上的用户名和用户空间不是一回事(如果用的 GitHub 就不存在这个问题),我这里用户名位置实际填入用户空间名(如果你也不知道什么是用户空间,那么恭喜你,直接写用户名就行了)。 加上时间,我们看下打印的过程: 接下来输入项目构建命令,将来 Jenkins 从 Gitee 上拉取代码下来之后,就执行该命令对项目进行打包: 最后,配置上传构建好的文件,并执行启动命令,如下: 配置的详细信息: 根据上图的配置,我们使用 root 用户登录,root 登录成功之后,默认进入到 /root 目录下,接下来会自动进入到 data 目录,然后我们的 jar 包就上传到这个位置上。 然后我们在应用服务器上也提前准备好一个 shell 脚本叫做 deploy.sh,位于 /root/data 目录下,这个脚本内容如下: export JAVA_HOME=/opt/java export PATH=$JAVA_HOME/bin:$PATH JAR_PATH=/root/data JARFILE=jenkins_demo-0.0.1-SNAPSHOT.jar ps -ef | grep $JARFILE | grep -v grep | awk '{print $2}' | xargs kill -9 java -jar $JAR_PATH/$JARFILE &gt; out.log &amp; if [ $? = 0 ];then sleep 30 tail -n 50 out.log fi 这个脚本其实很好理解,前面先配置一下环境变量,注意这个是应用服务器的环境变量,不是 Jenkins 的。 然后先检查一下,如果应用程序已经在运行了,就先将之停止掉。然后运行我们最新的 jar 即可。 另外,可以开启 SSH 操作日志,开启日志之后,就可以看到 Jenkins 中操作应用服务器其的过程了,特别是大家第一次配置的时候,容易出错,配置了日志,将来出错就知道什么原因导致的错误了。 配置方式如下图: 至此,这个项目就配置完成了。 保存之后,点击立即构建按钮,就可以开始构建了: 开始构建之后,可以点击构建按钮,查看构建过程: 点击控制台输出,可以查看整个构建过程: 构建完成后,来到应用服务器,执行 jps 命令查看运行的 Java 进程,就可以看到我们的应用程序已经跑起来了。 8. 自动构建 好了,现在我们的项目还不是自动构建,也就是当我们向 Gitee 上的代码仓库提交代码之后,并不会触发 Jenkins 的自动构建,得我们手动点击构建按钮,接下来我们再来继续配置,实现自动触发构建。 为了实现自动触发构建,我们需要修改两个地方。 8.1 修改 Jenkins 首先在 Jenkins 的当前项目中,配置一下触发构建的规则: 大家注意,在网页上 Jenkins 已经给出了将来要配置的 Webhook 的地址了,大家直接拷贝该地址即可。 8.2 配置 Webhook 接下来在 Gitee 的项目中,配置 WebHook,在当前项目中,选择管理选项卡,左边菜单点击 WebHooks,然后点击添加 WebHook。 由于 Jenkins 是要登录之后才可以操作的,处于公网的 Jenkins 我们也不能降至设置为匿名访问,所以这里我们将 Jenkins 的用户名密码放在请求地址中,最终地址类似这样:http://username:password@11.11.11.11:8088/xxxx。 好啦,这就行了,配置完成后,接下来我们向 Gitee 代码仓库提交代码,提交成功之后,我们去查看是否会触发 Jenkins 自动构建功能。 9. 小结 好啦,关于 Jenkins 还有很多好玩的用法,今天的文章限于篇幅我就先通过一个简单的案例来和大家分享一下 Jenkins 的基本用法,以便于小伙伴们对 Jenkins 建立一个直观的印象,更多的玩法,松哥将在后续的文章中和大家继续介绍,小伙伴们也可以留言说说你想看 Jenkins 怎么玩。 参考资料: https://www.redhat.com/zh/topics/devops/what-is-ci-cd

优秀的个人博客,低调大师

mica-auto 2.3.0 发布,支持 SpringBoot 2.7.0 新特性

更新记录 ✨ 支持 Spring boot 2.7.0 新特性 @AutoConfiguration。 ⬆️ 升级 Spring boot 到 2.7.0 注意:mica-auto 并不强制依赖 Spring boot,仅仅是组合了 spring-boot-configuration-processor依赖,方便使用。mica-auto 2.3.0 理论上支持 Spring boot 所有版本。 关于 Spring boot 2.7.0 @AutoConfiguration 1 @AutoConfiguration 注解 Spring boot 2.7.0 新增 @AutoConfiguration 注解,它用来替换 @Configuration 注解, 我们可以看到它组合了 @Configuration(默认 proxyBeanMethods = false 配置类不进行代理,可节省资源另外对 GraalVM 更加友好)、@AutoConfigureAfter 和 @AutoConfigureBefore 方便使用。 另外 Spring boot 2.7.0 开始推荐使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 替换 spring.factories 中的 EnableAutoConfiguration 配置。 2 老的 spring.factories EnableAutoConfiguration 配置 3 新的 AutoConfiguration.imports 配置 使用 mica-auto 2.3.0 在 Spring boot 2.7.x 的配置类中如果使用 @AutoConfiguration注解就会自动生成到新的 AutoConfiguration.imports 配置中,如果任然使用的是老的 @Configuration则任然会生成到 spring.factories中。 使用场景 Spring boot starter 开发利器,自动生成 spring.factories、AutoConfiguration.imports、spring-devtools.properties 配置。 多模块项目中的子项目,包名不同时的自动配置(主项目不建议添加)。 java spi 扩展自动生成配置。 建议关注如梦技术码云:https://gitee.com/596392912 ,更多微服务核心组件值得拥有。

优秀的个人博客,低调大师

牛逼,手撸一个SpringBoot缓存系统!

缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。 本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。 一、通用缓存接口 1、缓存基础算法 FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。 LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。 LRU(Least Recently Used),最近最少使用,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据移除。 2、接口定义 简单定义缓存接口,大致可以抽象如下: packagecom.power.demo.cache.contract;importjava.util.function.Function;/***缓存提供者接口**/publicinterfaceCacheProviderService{/***查询缓存**@paramkey缓存键不可为空**/<TextendsObject>Tget(Stringkey);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/<TextendsObject>voidset(Stringkey,Tobj);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime);/***移除缓存**@paramkey缓存键不可为空**/voidremove(Stringkey);/***是否存在缓存**@paramkey缓存键不可为空**/booleancontains(Stringkey);} 注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。 Get相关方法,注意多个参数的情况,缓存接口里面传人的Function,这是Java8提供的函数式接口,虽然支持的入参个数有限(这里你会非常怀念.NET下的Func委托),但是仅对Java这个语言来说,这真是一个重大的进步^_^。 接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存。 二、本地缓存 本地缓存,也就是JVM级别的缓存(本地缓存可以认为是直接在进程内通信调用,而分布式缓存则需要通过网络进行跨进程通信调用),一般有很多种实现方式,比如直接使用Hashtable、ConcurrentHashMap等天生线程安全的集合作为缓存容器,或者使用一些成熟的开源组件,如EhCache、Guava Cache等。本文选择上手简单的Guava缓存。 1、什么是Guava Guava,简单来说就是一个开发类库,且是一个非常丰富强大的开发工具包,号称可以让使用Java语言更令人愉悦,主要包括基本工具类库和接口、缓存、发布订阅风格的事件总线等。在实际开发中,我用的最多的是集合、缓存和常用类型帮助类,很多人都对这个类库称赞有加。 2、添加依赖 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency> 3、实现接口 packagecom.power.demo.cache.impl;importcom.google.common.cache.Cache;importcom.google.common.cache.CacheBuilder;importcom.google.common.collect.Maps;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.util.Map;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;importjava.util.function.Function;/**本地缓存提供者服务(GuavaCache)**/@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)@Qualifier("localCacheService")publicclassLocalCacheProviderImplimplementsCacheProviderService{privatestaticMap<String,Cache<String,Object>>_cacheMap=Maps.newConcurrentMap();static{Cache<String,Object>cacheContainer=CacheBuilder.newBuilder().maximumSize(AppConst.CACHE_MAXIMUM_SIZE).expireAfterWrite(AppConst.CACHE_MINUTE,TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次访问后的一段时间移出.recordStats()//开启统计功能.build();_cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE),cacheContainer);}/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=get(key,null,null,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=get(key,function,key,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=get(key,function,funcParm,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime){Tobj=get(key,function,key,expireTime);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime){Tobj=null;if(StringUtils.isEmpty(key)==true){returnobj;}expireTime=getExpireTime(expireTime);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);try{if(function==null){obj=(T)cacheContainer.getIfPresent(key);}else{finalLongcachedTime=expireTime;obj=(T)cacheContainer.get(key,()->{TretObj=function.apply(funcParm);returnretObj;});}}catch(Exceptione){e.printStackTrace();}returnobj;}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){set(key,obj,AppConst.CACHE_MINUTE);}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){if(StringUtils.isEmpty(key)==true){return;}if(obj==null){return;}expireTime=getExpireTime(expireTime);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);cacheContainer.put(key,obj);}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){if(StringUtils.isEmpty(key)==true){return;}longexpireTime=getExpireTime(AppConst.CACHE_MINUTE);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);cacheContainer.invalidate(key);}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}privatestaticLocklock=newReentrantLock();privateCache<String,Object>getCacheContainer(LongexpireTime){Cache<String,Object>cacheContainer=null;if(expireTime==null){returncacheContainer;}StringmapKey=String.valueOf(expireTime);if(_cacheMap.containsKey(mapKey)==true){cacheContainer=_cacheMap.get(mapKey);returncacheContainer;}try{lock.lock();cacheContainer=CacheBuilder.newBuilder().maximumSize(AppConst.CACHE_MAXIMUM_SIZE).expireAfterWrite(expireTime,TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次访问后的一段时间移出.recordStats()//开启统计功能.build();_cacheMap.put(mapKey,cacheContainer);}finally{lock.unlock();}returncacheContainer;}/***获取过期时间单位:毫秒**@paramexpireTime传人的过期时间单位毫秒如小于1分钟,默认为10分钟**/privateLonggetExpireTime(LongexpireTime){Longresult=expireTime;if(expireTime==null||expireTime<AppConst.CACHE_MINUTE/10){result=AppConst.CACHE_MINUTE;}returnresult;}} 4、注意事项 Guava Cache初始化容器时,支持缓存过期策略,类似FIFO、LRU和LFU等算法。 expireAfterWrite:最后一次写入后的一段时间移出。 expireAfterAccess:最后一次访问后的一段时间移出。 Guava Cache对缓存过期时间的设置实在不够友好。常见的应用场景,比如,有些几乎不变的基础数据缓存1天,有些热点数据缓存2小时,有些会话数据缓存5分钟等等。 通常我们认为设置缓存的时候带上缓存的过期时间是非常容易的,而且只要一个缓存容器实例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。 但是Guava Cache不是这个实现思路,如果缓存的过期时间不同,Guava的CacheBuilder要初始化多份Cache实例。 好在我在实现的时候注意到了这个问题,并且提供了解决方案,可以看到getCacheContainer这个函数,根据过期时长做缓存实例判断,就算不同过期时间的多实例缓存也是完全没有问题的。 三、分布式缓存 分布式缓存产品非常多,本文使用应用普遍的Redis,在Spring Boot应用中使用Redis非常简单。 1、什么是Redis Redis是一款开源(BSD许可)的、用C语言写成的高性能的键-值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。它可以被用作缓存、消息中间件和数据库,在很多应用中,经常看到有人选择使用Redis做缓存,实现分布式锁和分布式Session等。作为缓存系统时,和经典的KV结构的Memcached非常相似,但又有很多不同。 Redis支持丰富的数据类型。Redis的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等数据类型。对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。 Redis的数据类型: Keys:非二进制安全的字符类型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”这样包含空格和换行的key是不允许的。Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到Redis单线程操作模式,Value的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。 为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。 Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。 同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。 2、添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> 3、配置Redis 在application.properties配置文件中,配置Redis常用参数: ##Redis缓存相关配置#Redis数据库索引(默认为0)spring.redis.database=0#Redis服务器地址spring.redis.host=127.0.0.1#Redis服务器端口spring.redis.port=6379#Redis服务器密码(默认为空)spring.redis.password=123321#Redis连接超时时间默认:5分钟(单位:毫秒)spring.redis.timeout=300000ms#Redis连接池最大连接数(使用负值表示没有限制)spring.redis.jedis.pool.max-active=512#Redis连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0#Redis连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=8#Redis连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.jedis.pool.max-wait=-1ms 常见的需要注意的是最大连接数(spring.redis.jedis.pool.max-active )和超时时间(spring.redis.jedis.pool.max-wait)。Redis在生产环境中出现故障的频率经常和这两个参数息息相关。 接着定义一个继承自CachingConfigurerSupport(请注意cacheManager和keyGenerator这两个方法在子类的实现)的RedisConfig类: packagecom.power.demo.cache.config;importorg.springframework.cache.CacheManager;importorg.springframework.cache.annotation.CachingConfigurerSupport;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.cache.RedisCacheManager;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.StringRedisSerializer;/***Redis缓存配置类*/@Configuration@EnableCachingpublicclassRedisConfigextendsCachingConfigurerSupport{@BeanpublicCacheManagercacheManager(RedisConnectionFactoryconnectionFactory){returnRedisCacheManager.create(connectionFactory);}@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();//Jedis的Key和Value的序列化器默认值是JdkSerializationRedisSerializer//经实验,JdkSerializationRedisSerializer通过RedisDesktopManager看到的键值对不能正常解析//设置key的序列化器template.setKeySerializer(newStringRedisSerializer());////设置value的序列化器默认值是JdkSerializationRedisSerializer//使用Jackson序列化器的问题是,复杂对象可能序列化失败,比如JodaTime的DateTime类型////使用Jackson2,将对象序列化为JSON//Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);////json转对象类,不设置默认的会将json转成hashmap//ObjectMapperom=newObjectMapper();//om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);//jackson2JsonRedisSerializer.setObjectMapper(om);//template.setValueSerializer(jackson2JsonRedisSerializer);//将redis连接工厂设置到模板类中template.setConnectionFactory(factory);returntemplate;}////自定义缓存key生成策略//@Bean//publicKeyGeneratorkeyGenerator(){//returnnewKeyGenerator(){//@Override//publicObjectgenerate(Objecttarget,java.lang.reflect.Methodmethod,Object...params){//StringBuffersb=newStringBuffer();//sb.append(target.getClass().getName());//sb.append(method.getName());//for(Objectobj:params){//if(obj==null){//continue;//}//sb.append(obj.toString());//}//returnsb.toString();//}//};//}} 在RedisConfig这个类上加上@EnableCaching这个注解,这个注解会被Spring发现,并且会创建一个切面(aspect) 并触发Spring缓存注解的切点(pointcut)。据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。 cacheManager方法,申明一个缓存管理器(CacheManager)的bean,作用就是@EnableCaching这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法。keyGenerator方法,可以根据需求自定义缓存key生成策略。 而redisTemplate方法,则主要是设置Redis模板类,比如键和值的序列化器(从这里可以看出,Redis的键值对必须可序列化)、redis连接工厂等。 RedisTemplate支持的序列化器主要有如下几种: JdkSerializationRedisSerializer:使用Java序列化; StringRedisSerializer:序列化String类型的key和value; GenericToStringSerializer:使用Spring转换服务进行序列化; JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON; Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON; OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化; 注意:RedisTemplate的键和值序列化器,默认情况下都是JdkSerializationRedisSerializer,它们都可以自定义设置序列化器。 推荐将字符串键使用StringRedisSerializer序列化器,因为运维的时候好排查问题,JDK序列化器的也能识别,但是可读性稍差(是因为缓存服务器没有JRE吗?),见如下效果: 而值序列化器则要复杂的多,很多人推荐使用Jackson2JsonRedisSerializer序列化器,但是实际开发过程中,经常有人碰到反序列化错误,经过排查多数都和Jackson2JsonRedisSerializer这个序列化器有关。 4、实现接口 使用RedisTemplate,在Spring Boot中调用Redis接口比直接调用Jedis简单多了。 packagecom.power.demo.cache.impl;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.util.StringUtils;importjavax.annotation.Resource;importjava.io.Serializable;importjava.util.concurrent.TimeUnit;importjava.util.function.Function;@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)@Qualifier("redisCacheService")publicclassRedisCacheProviderImplimplementsCacheProviderService{@ResourceprivateRedisTemplate<Serializable,Object>redisTemplate;/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=get(key,null,null,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=get(key,function,key,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=get(key,function,funcParm,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime){Tobj=get(key,function,key,expireTime);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime){Tobj=null;if(StringUtils.isEmpty(key)==true){returnobj;}expireTime=getExpireTime(expireTime);try{ValueOperations<Serializable,Object>operations=redisTemplate.opsForValue();obj=(T)operations.get(key);if(function!=null&&obj==null){obj=function.apply(funcParm);if(obj!=null){set(key,obj,expireTime);//设置缓存信息}}}catch(Exceptione){e.printStackTrace();}returnobj;}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){set(key,obj,AppConst.CACHE_MINUTE);}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){if(StringUtils.isEmpty(key)==true){return;}if(obj==null){return;}expireTime=getExpireTime(expireTime);ValueOperations<Serializable,Object>operations=redisTemplate.opsForValue();operations.set(key,obj);redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){if(StringUtils.isEmpty(key)==true){return;}redisTemplate.delete(key);}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}/***获取过期时间单位:毫秒**@paramexpireTime传人的过期时间单位毫秒如小于1分钟,默认为10分钟**/privateLonggetExpireTime(LongexpireTime){Longresult=expireTime;if(expireTime==null||expireTime<AppConst.CACHE_MINUTE/10){result=AppConst.CACHE_MINUTE;}returnresult;}} 注意:很多教程里都讲到通过注解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)实现数据缓存,根据实践,我个人是不推崇这种使用方式的。 四、缓存“及时”过期问题 这个也是开发和运维过程中非常经典的问题。 有些公司写缓存客户端的时候,会给每个团队分别定义一个Area,但是这个只能做到缓存键的分布区分,不能保证缓存“实时”有效的过期。 多年以前我写过一篇结合实际情况的文章,也就是加上缓存版本,请猛击这里 ,算是提供了一种相对有效的方案,不过高并发站点要慎重,防止发生雪崩效应。 Redis还有一些其他常见问题,比如:Redis的字符串类型Key和Value都有限制,且都是不能超过512M,请猛击这里。还有最大连接数和超时时间设置等问题,本文就不再一一列举了。 五、二级缓存 在配置文件中,加上缓存提供者开关: ##是否启用本地缓存spring.power.isuselocalcache=1##是否启用Redis缓存spring.power.isuserediscache=1 缓存提供者程序都实现好了,我们会再包装一个调用外观类PowerCacheBuilder,加上缓存版本控制,可以轻松自如地控制和切换缓存,code talks: packagecom.power.demo.cache;importcom.google.common.collect.Lists;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importcom.power.demo.common.AppField;importcom.power.demo.util.ConfigUtil;importcom.power.demo.util.PowerLogger;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.util.List;importjava.util.UUID;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;importjava.util.function.Function;/**支持多缓存提供程序多级缓存的缓存帮助类**/@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)publicclassPowerCacheBuilder{@Autowired@Qualifier("localCacheService")privateCacheProviderServicelocalCacheService;@Autowired@Qualifier("redisCacheService")privateCacheProviderServiceredisCacheService;privatestaticList<CacheProviderService>_listCacheProvider=Lists.newArrayList();privatestaticfinalLockproviderLock=newReentrantLock();/***初始化缓存提供者默认优先级:先本地缓存,后分布式缓存**/privateList<CacheProviderService>getCacheProviders(){if(_listCacheProvider.size()>0){return_listCacheProvider;}//线程安全try{providerLock.tryLock(1000,TimeUnit.MILLISECONDS);if(_listCacheProvider.size()>0){return_listCacheProvider;}StringisUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);CacheProviderServicecacheProviderService=null;//启用本地缓存if("1".equalsIgnoreCase(isUseCache)){_listCacheProvider.add(localCacheService);}isUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);//启用Redis缓存if("1".equalsIgnoreCase(isUseCache)){_listCacheProvider.add(redisCacheService);resetCacheVersion();//设置分布式缓存版本号}PowerLogger.info("初始化缓存提供者成功,共有"+_listCacheProvider.size()+"个");}catch(Exceptione){e.printStackTrace();_listCacheProvider=Lists.newArrayList();PowerLogger.error("初始化缓存提供者发生异常:{}",e);}finally{providerLock.unlock();}return_listCacheProvider;}/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=null;//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){obj=provider.get(key);if(obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,funcParm);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,funcParm);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,longexpireTime){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,expireTime);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,expireTime);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,longexpireTime){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,funcParm,expireTime);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,funcParm,expireTime);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***设置缓存键值直接向缓存中插入或覆盖值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){provider.set(key,obj);}}/***设置缓存键值直接向缓存中插入或覆盖值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){provider.set(key,obj,expireTime);}}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){//key=generateVerKey(key);//构造带版本的缓存键if(StringUtils.isEmpty(key)==true){return;}for(CacheProviderServiceprovider:getCacheProviders()){provider.remove(key);}}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;//key=generateVerKey(key);//构造带版本的缓存键if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}/***获取分布式缓存版本号**/publicStringgetCacheVersion(){Stringversion="";booleanisUseCache=checkUseRedisCache();//未启用Redis缓存if(isUseCache==false){returnversion;}version=redisCacheService.get(AppConst.CACHE_VERSION_KEY);returnversion;}/***重置分布式缓存版本如果启用分布式缓存,设置缓存版本**/publicStringresetCacheVersion(){Stringversion="";booleanisUseCache=checkUseRedisCache();//未启用Redis缓存if(isUseCache==false){returnversion;}//设置缓存版本version=String.valueOf(Math.abs(UUID.randomUUID().hashCode()));redisCacheService.set(AppConst.CACHE_VERSION_KEY,version);returnversion;}/***如果启用分布式缓存,获取缓存版本,重置查询的缓存key,可以实现相对实时的缓存过期控制*<p>*如没有启用分布式缓存,缓存key不做修改,直接返回**/publicStringgenerateVerKey(Stringkey){Stringresult=key;if(StringUtils.isEmpty(key)==true){returnresult;}booleanisUseCache=checkUseRedisCache();//没有启用分布式缓存,缓存key不做修改,直接返回if(isUseCache==false){returnresult;}Stringversion=redisCacheService.get(AppConst.CACHE_VERSION_KEY);if(StringUtils.isEmpty(version)==true){returnresult;}result=String.format("%s_%s",result,version);returnresult;}/***验证是否启用分布式缓存**/privatebooleancheckUseRedisCache(){booleanisUseCache=false;StringstrIsUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);isUseCache="1".equalsIgnoreCase(strIsUseCache);returnisUseCache;}} 单元测试如下: @TestpublicvoidtestCacheVerson()throwsException{Stringversion=cacheBuilder.getCacheVersion();System.out.println(String.format("当前缓存版本:%s",version));StringcacheKey=cacheBuilder.generateVerKey("goods778899");GoodsVOgoodsVO=newGoodsVO();goodsVO.setGoodsId(UUID.randomUUID().toString());goodsVO.setCreateTime(newDate());goodsVO.setCreateDate(newDateTime(newDate()));goodsVO.setGoodsType(1024);goodsVO.setGoodsCode("123456789");goodsVO.setGoodsName("我的测试商品");cacheBuilder.set(cacheKey,goodsVO);GoodsVOgoodsVO1=cacheBuilder.get(cacheKey);Assert.assertNotNull(goodsVO1);version=cacheBuilder.resetCacheVersion();System.out.println(String.format("重置后的缓存版本:%s",version));cacheKey=cacheBuilder.generateVerKey("goods112233");cacheBuilder.set(cacheKey,goodsVO);GoodsVOgoodsVO2=cacheBuilder.get(cacheKey);Assert.assertNotNull(goodsVO2);Assert.assertTrue("两个缓存对象的主键相同",goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));} 一个满足基本功能的多级缓存系统就好了。 在Spring Boot应用中使用缓存则非常简洁,选择调用上面包装好的缓存接口即可。 StringcacheKey=_cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid."+request.getGoodsId());GoodsVOgoodsVO=_cacheBuilder.get(cacheKey,_goodsService::getGoodsByGoodsId,request.getGoodsId()); 到这里Spring Boot业务系统开发中最常用到的ORM,缓存和队列三板斧就介绍完了。 在开发的过程中你会发现,Java真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。 来源:cnblogs.com/jeffwongishandsome 本文分享自微信公众号 - IT老哥(dys_family)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册