一次上线完成后,观察线上日志,发现很多死锁异常(org.springframework.dao.DeadlockLoserDataAccessException)。
mysql 提示信息:Deadlock found when trying to get lock; try restarting transaction。
这是一次并发插入/更新引发的死锁案例
事务1 | 事务2 | 备注 |
---|---|---|
INSERT INTO country (countryname, countrycode) VALUES (‘Angola’,’AO’) |
||
INSERT INTO country (countryname, countrycode) VALUES (‘Brazil’,’BR’) |
正常执行,相安无事 | |
UPDATE country SET countryname = ‘Angola’ WHERE countryname = ‘AO’ |
阻塞。事务1等待事务2 释放锁 | |
UPDATE country SET countryname = ‘Brazil’ WHERE countryname = ‘BR’ |
DEADLOCK。事务2 等待事务1 释放锁 |
-- 查询死锁日志
SHOW ENGINE innodb STATUS
------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-06-02 19:37:26 9ec
*** (1) TRANSACTION:
TRANSACTION 11187331, ACTIVE 5 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 312, 21 row lock(s), undo log entries 1
UPDATE country SET countryname = 'Angola' WHERE countryname = 'AO'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 515 page no 3 n bits 88 index `PRIMARY` of table `test`.`country` trx id 11187331 lock_mode X waiting
*** (2) TRANSACTION:
TRANSACTION 11187330, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
UPDATE country SET countryname = 'Brazil' WHERE countryname = 'BR'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 515 page no 3 n bits 88 index `PRIMARY` of table `test`.`country` trx id 11187330 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 515 page no 3 n bits 88 index `PRIMARY` of table `test`.`country` trx id 11187330 lock_mode X waiting
*** WE ROLL BACK TRANSACTION (2)
日志怎么看?
上述的 innodb 日志已经精简过了。
其中列出了死锁相关的两个事务*** (1) TRANSACTION
。
引发死锁的事务会列出持有的锁 HOLDS THE LOCK(S)
lock_mode X locks rec but not gap。 这对应的就是insert 语句的加锁。
最后,WE ROLL BACK TRANSACTION (2)
使用Mysql 客户端,通过两个客户端执行SQL 来模拟并发。
-- 查询事务隔离级别 (Transaction Isolation Levels)
SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
client1 | client2 | 备注 |
---|---|---|
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; | ||
START TRANSACTION; |
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; |
事务开始前才能修改事务隔离级别 |
UPDATE country SET countrycode =’foo’ WHERE Id =13; |
||
UPDATE country SET countrycode =’foo’ WHERE Id =13; |
/* SQL错误(1205):Lock wait timeout exceeded; try restarting transaction */ | |
SELECT * FROM country WHERE Id =13; |
/* SQL错误(1205):Lock wait timeout exceeded; try restarting transaction */ |
注意: 示例中是根据主键,mysql 是行锁,如果update 不是同一行数据,不会发生锁冲突。如果非索引更新,那么就是表锁,表事务串行。
client1 | client2 | 备注 |
---|---|---|
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; | ||
START TRANSACTION; |
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
START TRANSACTION; |
||
UPDATE country SET countrycode =’foo-2’ WHERE Id =14; |
||
SELECT * FROM country WHERE Id =14; |
读写不阻塞。读取的是旧值。 | |
COMMIT; | ||
SELECT * FROM country WHERE Id =14; |
client2 读取的countrycode 仍然是旧的。 |
client1 | client2 | 备注 |
---|---|---|
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | ||
START TRANSACTION; |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | |
START TRANSACTION; |
||
UPDATE country SET countrycode =’foo-3’ WHERE Id =14; |
||
SELECT * FROM country WHERE Id =14; |
读取的是旧值。 | |
COMMIT; | ||
SELECT * FROM country WHERE Id =14; |
读取的是client1更新的值。 |
client1 | client2 | 备注 |
---|---|---|
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | ||
START TRANSACTION; |
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | |
START TRANSACTION; |
||
UPDATE country SET countrycode =’foo-4’ WHERE Id =14; |
||
SELECT * FROM country WHERE Id =14; |
读取的是client1更新的值。 | |
ROLLBACK; | ||
SELECT * FROM country WHERE Id =14; |
读取的是client1更新前的值。 |
事务的隔离级别(access mode)是声明事务间数据可见级别的。
无论什么样的级别,对于同一条数据更新,写锁,肯定是互斥的。
These characteristics set the transaction isolation level or access mode.
The isolation level is used for operations on InnoDB tables. The access mode may be specified as of MySQL 5.6.5 and indicates whether transactions operate in read/write or read-only mode.
最近读源码,发现包装模式的写法案例。虽然不是严格的包装设计模式,但是能达到封装和适配的效果,简化使用方式和功能扩展。
这种编程模式非常实用,实际开发过程中可以借鉴。
在 Spring 封装javamail 的实现中,有个MimeMessageHelper 类。
通过包装MimeMessage 对象,提供简便的操作API。同时,镜像操作MimeMessage对象,对MimeMessage 直接赋值的操作。
/**
* 虽然名为Helper, 并不是常用的static Helper。Helper 提供的简便API调用,会直接映射到mimeMessage 对象上
* 使用方式:new MimeMessageHelper(mimeMessage);
* @see org.springframework.mail.javamail.MimeMessageHelper
*/
public MimeMessageHelper(MimeMessage mimeMessage) {
this(mimeMessage, null);
}
借鉴: 对于复杂的三方插件或者类库封装,可以参考spring 对javamail 的适配。
SpringBoot 默认支持以下路径的文件作为 web 静态资源。
By default, Spring Boot serves static content from a directory called
/static
(or/public
or/resources
or/META-INF/resources
) in the classpath or from the root of theServletContext
.
Developing Web Applications ➡Spring MVC Framework➡Static Content
org.springframework.web.servlet.resource.ResourceHttpRequestHandler
serves static resources optimized for superior browser performance (according to the guidelines of Page Speed, YSlow, etc.)
org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
Stores registrations of resource handlers for serving static resources such as images, css files and others through Spring MVC including setting cache headers optimized for efficient loading in a web browser.
org.springframework.boot.autoconfigure.web.ResourceProperties
org.springframework.boot.autoconfigure.web.WebMvcProperties
自动配置入口:org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
配置实现:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// 此处的cache 是指 HTTP协议的 Cache-Control
Integer cachePeriod = this.resourceProperties.getCachePeriod();
// 默认:/**, spring.mvc.static-path-pattern
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
// 默认: [/META-INF/resources/, /resources/, /static/, /public/]
.addResourceLocations(this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
# relocating all resources to /resources/**
spring.mvc.static-path-pattern=/resources/**
静态资源目录配置
spring.web.resources.static-locations=/webjars/**
<mvc:default-servlet-handler/>
<mvc:resources location="/static/" mapping="/static/**" cache-period="864000"/>
/**
- 查找占位符匹配的后缀索引。
- 因为Spring支持嵌套的占位符表示,所以配对的查找是这个方法核心要解决的
- 逻辑:遍历字符串buf,匹配遇到的占位符前缀和后缀,如果是配套的后缀,则返回索引。
- 因为有嵌套占位符的情况,需要一个临时的变量记录内嵌占位符的出现次数,通过成对匹配的计算(出现前缀加1,出现后缀减1),防止错误返回内嵌占位符的后缀索引。
- @param buf 配置字符串,比如:${foo:${defaultFoo}}
- @param startIndex 占位符在 buf中的index,初始值 = buf.indexOf("${");
*/
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + this.placeholderPrefix.length();
// 嵌套的占位符出现次数标识变量,出现前缀加1,出现后缀减1。
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
// 检测后缀,两种情况
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
// 1. 该后缀属于内嵌占位符,继续遍历
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
}
// 2. 目标占位符的后缀
else {
return index;
}
}
// 检测前缀,证明遇到的了内嵌占位符
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
}
else {
index++;
}
}
return -1;
}
/**
* Test whether the given string matches the given substring
* at the given index.
* 思路:以被查询的字符串,循环匹配每个字符,遇到不符合直接返回不匹配。
* 匹配查找和数据库的join 查询,小表驱动大表有异曲同工之处
* @param str the original string (or StringBuilder)
* @param index the index in the original string to start matching against
* @param substring the substring to match at the given index
*/
public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
// 遍历 toFind 字符串
for (int j = 0; j < substring.length(); j++) {
int i = index + j;
// 如果原字符串不包含(长度不够)或者字符不匹配,立即失败
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
return false;
}
}
return true;
}