常见使用问题
[1] Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
该问题通常在使用 JPA 或 Hibernate 的 saveOrUpdate
或类似的方法时出现。
简单解释这个问题的原因就是:saveOrUpdate
或类似的方法要求实体的 ID 为 null 时才执行 SAVE
操作 (对应数据库的 Insert),如果 ID 不为 null,就默认数据库中已经存在该调数据,在这种情况下就会执行 UPDATE
操作 (对应数据库的 Update)。正常通过 saveOrUpdate
保存实体数据的时候是新增,但如果此时实体的 ID 不为 null,saveOrUpdate
保存实体数据时就会改为 UPDATE
操作,但是数据表中并不存在以 ID 值作为主键的数据,那么更新操作失败,所以出现异常。
Dante Cloud 出现此问题,原理与上面所述的"问题原理"基本类似。
主要原因是服务在启动时,会扫描接口数据作为权限,汇总保存至 sys_authority 数据表中,由于增加的缓存的支持,所以在保存同时会向缓存中存储缓存数据。很多情况出现该问题,都是因为第一次部署不熟悉,要么发现操作步骤出错,要么发现配置错误等,在第一次表建好之后,清理了已有的表重新启动服务打算重新建。因为在第一次启动服务时,就会进行一次权限数据的汇总,缓存中就已经存在了数据。将数据表清理之后,再次启动服务,服务会首先读取缓存,发现缓存中有数据就认为是 UPDATE
操作,而此时数据库中并没有对应数据,所以导致跑错。
发现服务运行出现 Batch update returned unexpected ...
错误,首先将所有服务停止,清理 Redis 中已经存储的缓存数据,再次启动服务即可。
[2] Failed to bind properties under 'spring.datasource.password' to java.lang.String
所有基础设施搭建完成、代码编译完成之后,再运行服务时,有时会遇到服务运行抛出Failed to bind properties under 'spring.datasource.password' to java.lang.String
错误,导致无法运行的情况。出现这种问题的原因有很多,例举几种常见情况:
因为服务相关的所有配置,绝大部分都是通过读取 Nacos 获得。如果 Nacos 无法正常访问,当然就会导致服务无法运行。因为很多 Spring Boot 或 Spring Cloud 组件的运行依赖于正确的配置信息。
Nacos 无法正常访问的情况就会有很多种,比如:
- Nacos 运行异常:连 Nacos 后台管理页面也无法打开,这种情况就需要检查 Nacos 的安装是否正确
- 网络不通:很多情况下会选择服务与 Nacos 不再同一台机器中,网络不通就会导致无法访问。
- 配置错误:工程根目录下的 pom.xml 中,需要对 Nacos 的访问地址进行配置,配置错误当然也无法访问。(需要注意的是 pom 文件中 Nacos 配置完或者修改后,要重新编译代码才能正常访问,具体原因参见多环境配置章节)
- Nacos Namespace 使用不正确:工程根目录下的 pom.xml 中,除了需要配置 Nacos 地址外,还有一项配置
<config.namespace>
。如果在 Nacos 中,将本系统的配置导入到了某个命名空间中,那么就要在此处配置所使用命名空间的 ID;如果没有使用任何命名空间,那么该配置就留空。
[3]Redis 密码包含特殊字符无法连接
既然称之为特殊字符,大多都是特殊用途的符号。比如说 “@”,在很多连接协议中都是作为分隔符存在。在很多中间件或数据库之类的软件中,对于密码中包含的特殊字符也会有不同的要求。
Redis 连接密码除了极特别特殊字符外(具体是什么还是自己尝试吧),也是允许包含一些特殊字符的。
Dante Cloud 用户也曾遇到 Redis 密码中包含特殊字符,导致服务无法正常连接 Redis 而导致服务启动出错的问题。一下就是出错密码的示例:
mg3y9^3Wkp*g&ZBc.qM2
正常情况下,在 Spring Boot 环境的配置文件中,指定 Redis 的密码配置,Spring Data Redis 也会进行一定的转义,保证密码可用。本系统无法使用该密码,并非是 Spring Boot 的转义没有生效,而是因为系统中还集成了 JetCache。JetCache 在使用 Lettuce 进行 Redis 连接时,采用的是 url
协议进行 Redis 连接。
url:
- redis://password@ip:port
这就导致包含特殊字符的 Redis 密码无法正常连接 Redis。
而 JetCache 作者也表示,不打算支持特殊字符的转义。
也尝试过对密码进行转义,但目前没有成功。如果你有什么好的解决办法,欢迎提 ISSUE 或 PR。
[4]能不能不用 Kafka(或消息队列)
为什么要用消息队列
消息队列是多系统集成的多种方式中,比较好的一种方式。既规避了 ETL 集成方式时效性差的问题,又规避了纯页面或者纯接口集成方式出发机制不理想的问题,而且将多系统集成的耦合性降到了最低。
微服务平台看似是一个完整的系统,本质还是多个系统(服务)间的整合及配合。因此,消息队列是必不可少的一个组成部分。
目前,就作者已知的或已了解过的开源微服务框架,还没有发现那个系统不用消息队列的。(不一定都是用 Kafka,但都是用了消息队列)
确实不想用怎么办
因为本系统没有使用最基础的 Kafka,而是集成了 spring-cloud-bus
。而 spring-cloud-bus
又依赖于 spring-cloud-stream
、spring-integration
和 spring-kafka
等多种组件。这就导致无法简单的通过修改配置文件来彻底关闭 Kafka
。
友情提示
spring-integration
是个神器的存在,只要你代码的上下文中包含 spring-cloud-stream
或 spring-kafka
等依赖,连 Swagger
都会主动去连接消息队列。
所以想要彻底不使用 Kafka (消息队列),只能通过去除依赖包的方法。
具体操作的步骤是:
- 在
herodotus-cloud-message
包中,删除spring-cloud-starter-bus-kafka
依赖。 - 把
herodotus-cloud-message
包中涉及到 Kafka 相关引用的代码删除。 - 将
herodotus-cloud-bpmn-ability
包中,@KafkaListener
注解以及代码引用删除 - 重新编译代码运行。
警告
消息队列是本系统的核心组件,因目前关联的内容还不算特别多。非要剔除消息队列,可能导致部分功能的无法使用或者出现后续版本不兼容的情况。
所以请三思而后行!切记!
[5]为什么没有看到 Seata
文档中,有提到本系统集成了 Seata
,问什么在代码中,连 Seata
的安装信息都没有看到。
这个问题的主要原因是:
- 当前开源版本基础的服务本身比较少,没有什么具体的、合适的场景必须用到
Seata
。即使加上就是多装一个东西,最多些点 Demo 代码。 - 在这种情况下,强制加上
Seata
只会增加本系统搭建资源成本、时间成本以及复杂程度。特别是学习使用本系统的朋友,各种能力程度和技能水平的都有,有些朋友搭建基本内容都有困难,多一项内容就更多了一份难度。 - 微服务框架本身涉及的内容就非常多,个人观点非必要、非必需的内容没有必要增加太多。确实需要时,集成一个
Seata
也非常简单,真正能用起来的东西才是有价值的。就像做单体项目时,又有多少人真真正正的使用过“事务”?
提示
这里引用一篇个人认为比较好的介绍分布式事务文章的结论供参考:
上边简单介绍了 2PC、3PC、TCC、MQ、Seata 这五种分布式事务解决方案,还详细的实践了 Seata 中间件。但不管我们选哪一种方案,在项目中应用都要谨慎再谨慎,除特定的数据强一致性场景外,能不用尽量就不要用,因为无论它们性能如何优越,一旦项目套上分布式事务,整体效率会几倍的下降,在高并发情况下弊端尤为明显。
[6]登录系统提示“您没有权限拒绝访问”
前端工程正常启动后,弹出错误提示“您没有权限,拒绝访问”,如下图所示:
出现该问题,请首先检查是否严格按照文档描述的内容和步骤进行的后端部署,特别是有没有执行 【重置缓存】 这一步。
具体问题的原理,和重置缓存
章节描述的基础原理一致。初次搭建环境,需要初始化数据库,这里就包括用户、角色、权限以及相关的关联关系。不管是以手动方式还是自动方式进行的数据库初始化,本质都是直接执行 sql 脚本。而使用 Spring Authorization Server
和 Spring Security
做微服务,标准合理的方式是在各个服务中进行接口的鉴权。各服务的鉴权数据是以缓存的形式存储在各个服务中。因为使用了多级缓存,在数据库中执行 sql 脚本,是无法触发缓存数据生成的,这就导致即前后端成功运行之后,登录前端会出现没有权限的提示,因为缓存中没有权限数据。
- 停止所有服务
- 清空 Redis 缓存数据
- 再次运行所有服务
单体版也是如此
[7]客户端身份验证失败
前端工程正常启动后,弹出错误提示“客户端身份验证失败”,如下图所示:
具体问题的原理,与下面问题的原理一致。想要了解原理,可以看下面问题中的"系统设计逻辑",不关心可以跳过。
问题原因
这个问题原因就是:没有初始化数据库
。
第一次搭建本系统,还是建议耐心、细致的看文档,相关的内容都有说。
[8]登录框无法输入用户名和密码
前端工程正常启动后,出现登录框无法输入用户名和密码的情况。通过浏览器控制台查看,控制台中会出现 412
错误,如下所示:
POST http://localhost:8847/herodotus-cloud-uaa/open/identity/session 412 (Precondition Failed)
系统设计逻辑
- 为了提高系统的安全性,本系统混合 SM2(非对称) 和 SM4(对称加密) 算法,基于自定义注解和数字信封技术,设计接口数据前后端加密传输逻辑。实现 Sm4 KEY 动态生成、前后端数据加密传输、一人一钥的安全机制。
2.7.X 版本之前,默认使用的是 RSA + AES 算法实现,2.7.X 版本之后改为使用 SM2 + SM4,仍旧支持 RSA + AES 可以通过修改配置进行变更
- 由于 Vue 的 Session 存在变化的问题,导致使用 Vue Session 无法确认唯一性,因此系统中增加了自定义 Session 机制。本系统的安全机制,就是基于自定义 Session 机制实现。每次使用系统时,根据自定义 Session,后端动态生成秘钥实现数据的加密传输。
- 因为,自定义 Session 的创建和秘钥的传输,使用的是
开放
接口,即无须认证既可以使用的接口。所以为了进一步提升安全性,open/identity/session
接口,增加了对客户端的认证逻辑,如果是非法客户端或者客户端未验证通过,open/identity/session
的接口是无法返回正确信息,输入用户名和密码也无法正确解密,所以输入框被禁用。
出现问题的原因
open/identity/session
接口要验证客户端的合法性,使用的是 clientId
和 clientSecret
信息,在系统后端进行验证。客户端信息验证失败,通常是以下几方面原因:
- 后端 IP 地址错误,前端无法正常连接。
- 数据库中有数据,
clientId
和clientSecret
信息不匹配。 - 数据库未正确初始化数据,
oauth2_registered_client
表中没有数据,所以clientId
和clientSecret
信息始终是不匹配的。
如何解决
- 检查前端工程中,
.env.development
或.env.production
文件中配置的后端 IP 地址是否正确。 - 初次搭建本项目的过程中,在工程尚未正确运行时,除了在前端修改后端访问 IP 外,不要擅自修改其它已经配置好的参数。
- 检查数据库是否正确进行了数据初始化操作(检查数据表里是否有数据,特别是
oauth_client_details
)。
提示
大多数情况,要么是没有耐心和认真的看文档,不按照文档操作;要么是在系统还没有运行成功,搭建的过程中,就按照自己的想法和理解进行修改和参数调整,导致运行失败。
为什么控制台中看到返回的是412
错误
412
错误是源于本项目中的自定义错误体系。为了更快捷更准确的定位错误,本系统自定义错误体系与 Http 协议中的状态进行了有机结合,并尽可能借鉴 Http 状态的含义。
本系统自定义错误体系借鉴了 Http 412
(Precondition Failed) 状态的含义: 未满足前提条件。看到这一类错误,通常代表是因为前序的条件或者参数为准备充足,导致无法正确运行。
[9]初次 install 或者 package 工程很慢
Maven 使用的基本逻辑
在 Maven 的术语中,仓库是一个位置(place)。
Maven 仓库是项目中依赖的第三方包库,这个库所在的位置叫做仓库。
Maven 仓库有三种类型:
- 本地(local)
- 中央(central)
- 远程(remote)
Maven 的本地仓库,在安装 Maven 后并不会创建,它是在第一次执行 maven 命令的时候才被创建。
运行 Maven 的时候,Maven 所需要的任何构件都是直接从本地仓库获取的。如果本地仓库没有,它会首先尝试从远程仓库下载构件至本地仓库,然后再使用本地仓库的构件。
为什么初次 Install 会很慢?
- 可能本工程中所使用的包并不是你常用的包,本工程的包尽可能用最新版本的代码,所以第一次使用会下载很多的包。根据自身网络的不同,依赖包的下载速度也会不同,编译所需要的时间也会不同。
- 可能目前你所使用的,还是默认的 Maven 源,默认的 Maven 源是国外源,通常访问都会很慢,甚至有访问响应超时的情况。根据自身网络的不同,依赖包的下载速度也会不同,编译所需要的时间也会不同。
- 为了方便调试单体版以及支持
Spring Boot Admin
查看 git 信息和 Docker 打包,在编译过程中工程默认是开启了源代码打包
、Git信息打包
以及资源包拷贝
功能的,因为有更多的操作,势必会增加编译打包的时间。
提升编译速度的办法
- 提现下载好依赖包
初次使用本项目,在执行 Install
或 Package
命令之前,打开 Idea Maven 面板,点击工具栏的刷新按钮,手动触发依赖包的下载,等所有包下载完成之后再在执行 Install
或 Package
命令进行编译。刷新方式如下图所示:
提示
可能会出现,虽然依赖包都下载完成了,但是 Idea Maven 面板中,仍旧存在一些依赖包“飘红”的现象。这是 Idea 没有实时读取完整依赖包的问题,只要能正常编译通过,就不会有任何问题。下次再打开 Idea 通常就不会有标红的内容。
- 更改为使用腾讯 Maven 源。
如果你熟悉怎么改请略过,如果不熟悉,在线文档中:【环境准备】-> 后端环境搭建配置
- 关闭编译辅助工具
可以在工程根目录中的 pom.xml 中,将对应 profiles 里面的 <skip.build.source.package>、<skip.copy.docker.resource>、<skip.build.git.commit.info> 三个属性值改为 true。这样可以减少打包内容,提升打包速度。
<profile>
······
<properties>
······
<!--跳过构建源代码包-->
<skip.build.source.package>false</skip.build.source.package>
<!--不copy代码包到docker构建目录-->
<skip.copy.docker.resource>false</skip.copy.docker.resource>
<!--不执行git commit 构建-->
<skip.build.git.commit.info>false</skip.build.git.commit.info>
······
</properties>
</profile>
- 修改为多线程编译
在 IDEA 中,默认是使用单线程进行 Maven 编译的。可以将其修改为多线程编译方式,可以提高一定的编译速度。
在 Idea 中,找点击 Settings... -> Build,Execution,Deployment -> Build Tools -> Maven
。在 Maven 配置面板中。将 Thread Count 的值设置为 1C。这样可以进行多线程打包,提升打包速度,如下图所示:
[10]found character '@' that cannot start any token. (Do not use @ for indentation)
启动服务时,出现以下错误
16:01:05.726 [main] ERROR org.springframework.boot.SpringApplication - Application run failed
org.yaml.snakeyaml.scanner.ScannerException: while scanning for the next token
found character '@' that cannot start any token. (Do not use @ for indentation)
in 'reader', line 7, column 13:
active: @profile.name@
^
at org.yaml.snakeyaml.scanner.ScannerImpl.fetchMoreTokens(ScannerImpl.java:439)
at org.yaml.snakeyaml.scanner.ScannerImpl.checkToken(ScannerImpl.java:248)
at org.yaml.snakeyaml.parser.ParserImpl$ParseBlockMappingValue.produce(ParserImpl.java:633)
at org.yaml.snakeyaml.parser.ParserImpl.peekEvent(ParserImpl.java:165)
at org.yaml.snakeyaml.comments.CommentEventsCollector$1.peek(CommentEventsCollector.java:59)
at org.yaml.snakeyaml.comments.CommentEventsCollector$1.peek(CommentEventsCollector.java:45)
at org.yaml.snakeyaml.comments.CommentEventsCollector.collectEvents(CommentEventsCollector.java:140)
at org.yaml.snakeyaml.comments.CommentEventsCollector.collectEvents(CommentEventsCollector.java:119)
at org.yaml.snakeyaml.composer.Composer.composeScalarNode(Composer.java:221)
at org.yaml.snakeyaml.composer.Composer.composeNode(Composer.java:191)
at org.yaml.snakeyaml.composer.Composer.composeKeyNode(Composer.java:309)
at org.yaml.snakeyaml.composer.Composer.composeMappingChildren(Composer.java:300)
at org.yaml.snakeyaml.composer.Composer.composeMappingNode(Composer.java:288)
at org.yaml.snakeyaml.composer.Composer.composeNode(Composer.java:195)
at org.yaml.snakeyaml.composer.Composer.composeValueNode(Composer.java:313)
at org.yaml.snakeyaml.composer.Composer.composeMappingChildren(Composer.java:304)
at org.yaml.snakeyaml.composer.Composer.composeMappingNode(Composer.java:288)
at org.yaml.snakeyaml.composer.Composer.composeNode(Composer.java:195)
at org.yaml.snakeyaml.composer.Composer.composeValueNode(Composer.java:313)
at org.yaml.snakeyaml.composer.Composer.composeMappingChildren(Composer.java:304)
at org.yaml.snakeyaml.composer.Composer.composeMappingNode(Composer.java:288)
at org.yaml.snakeyaml.composer.Composer.composeNode(Composer.java:195)
at org.yaml.snakeyaml.composer.Composer.getNode(Composer.java:115)
at org.yaml.snakeyaml.constructor.BaseConstructor.getData(BaseConstructor.java:135)
at org.springframework.boot.env.OriginTrackedYamlLoader$OriginTrackingConstructor.getData(OriginTrackedYamlLoader.java:99)
at org.yaml.snakeyaml.Yaml$1.next(Yaml.java:512)
该问题主要是由于 bootstrap.yml
中使用了 @ @
变量,而在 target
目录中生成的 bootstrap.yml
中的 @ @
变量没有被正确替换。
解决办法
办法一
使用 mvn install
或 mvn package
命令重新编译工程。
办法二
在 IDEA 中,打开Maven
面板,点击刷新按钮。如下图所示:
[11]服务启动后,无法连接 Redis 抛错
背景
Dante Cloud 基础服务在启动时必须要连接 Redis,连接成功后才能正常运行。
目前涉及到 Redis 的内容,主要包括两部分:
- 常规的数据缓存特别像 Session、验证码一类的临时缓存,这一类缓存根据场景以及实现要求的不同,有些会直接使用 Redis;
- 数据共享缓存,像权限数据、CRUD 数据等缓存,这一类缓存除了要实现数据缓存的目的之外,还要解决缓存数据在多服务、多实例环境下数据共享同步等问题。因此采用的是 JetCache 多级缓存组件。
因本项目更倾向于贴近 Spring 生态,尽量使用 Spring 生态主打或推荐的相关组件。所以不管是直接访问 Redis 还是集成 JetCache,底层数据访问客户端组件均使用的是 lettuce。也正因为如此,Spring 生态相关组件自身的不足也会体现在 Dante Cloud 中。
问题原因
服务启动后无法连接 Redis,除了网络问题导致的无法连接以外,最主要的诱因就是:Redis 密码中包含特殊字符。
包含特殊字符的密码导致 Redis 无法连接,原因大概出自两方面:
- 一方面是 Spring Boot 基本规范导致。Spring Boot Yaml 对特殊字符是有处理要求的,Yaml 配置信息中,如果包含以下特殊字符必须要进行转义
:, {, }, [, ], ,, &, *, #, ?, |, -, <, >, =, !, %, @, `
- 另一方面是基础组件自身处理机制导致。有些组件是采用 uri 的方式进行 Redis 连接配置,而有些组件使用 ip + 端口等方式处理。所以就可能存在,同样的密码,使用 Jedis、Redisson 等组件是正常的,换到 Dante Cloud 所使用的 lettuce 就不正常;甚至可能使用 lettuce 是正常的,换到 JetCache 又会有问题。
解决方法
方法一
这个方法最直接最简单,就是修改 Redis 密码,去掉密码中的某个或者全部特殊字符。
提示
很多人都期望通过增加 Redis 密码复杂度来提升 Redis 的安全性。不可否认这确实能提升一定的安全性,但是毕竟 Redis 的密码安全机制太过简单,所以个人建议不要太过于依赖于此,网络层面或者物理层面保证 Redis 的相对隔离才能靠谱。(换句话说 Redis 密码复杂度更像是心理安慰,不能从本质上提升多大的安全性,所以就不要太纠结密码中要不要包含特殊字符的问题)
方法二
对密码中的特殊字符进行转义,以保证可以正确被读取和使用。
- 在 Spring Boot Yaml 方面,如果存在特殊字符就需要对密码字符串进行转义,例如下例中密码包含了特殊字符 “#”,这种写法是会出现运行错误问题的
spring:
#data source connection
datasource:
url: jdbc:mysql://localhost:3306/vaquarkhan
username: rootadmin
password: root#
需要对特殊字符进行转义:
可以用引号包裹字符串进行转义
"root#"
或者用反斜杠
root\#
完整的例子
spring:
#data source connection
datasource:
url: jdbc:mysql://localhost:3306/vaquarkhan
username: rootadmin
password: "root#"
- 对于像 JetCache 一样使用 uri 进行 Redis 连接配置的,那么特殊字符的转义,就要参考 url.encode 的方式,例如:
符号 | - | 转义结果 | 符号 | - | 转义结果 |
---|---|---|---|---|---|
空格 | - | %20 | / | - | %2F |
" | - | %22 | : | - | %3A |
# | - | %23 | ; | - | %3B |
% | - | %25 | < | - | %3C |
& | - | %26 | = | - | %3D |
( | - | %28 | > | - | %3E |
) | - | %29 | ? | - | %3F |
+ | - | %2B | @ | - | %40 |
, | - | %2C | \ | - | %5C |
一个完成的参考示例,需要转义,spring Data redis 使用'',而 jetcache 使用 URL 转义
spring:
application:
redis:
password: "123@!"
jetcache:
remote:
test:
uri: redis://123%40%21@${spring.redis.host}:${spring.redis.port}/${spring.redis.database}
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
[12]NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH < user >
使用spring boot data redis
链接redis sentinel
集群,会报此错。
原因是因为redis sentinel
集群的sentinel
配置文件没有配置密码,sentinel
配置密码的方式和 redis 的密码配置方式一样,配置文件中加上requirepass <密码>
就行。
另外,sentinel
中的sentinel auth-pass <master> <pass>
不是给sentinel
配置密码,而是sentinel
链接redis master
的密码