使用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/publicor/resourcesor/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;
}
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name ‘fooController’:
Failed to introspect bean class [com.foo.FooController] for lookup method metadata: could not find class that it depends on;
nested exception is java.lang.NoClassDefFoundError: com/foo/Result
// 该方法的返回类型 ResponseResult extend Result
@RequestMapping("/getList")
public ResponseResult<Foo> getList(@RequestBody Query params){
//...
}
根据错误提示,在代码中并没有找到 Result 类直接使用。
再进一步看日志,introspect bean 出现的问题,推测是进行反射的操作异常。
通过查看方法的返回类型,其父类就是Result,但是在当前的工程中,并没有引入对应的maven 依赖。
结果就是编译通过,但是运行时异常。
引入需要的jar 到当前的工程(classpath)。
上述提到的introspect bean 过程,在Spring 中的相关源码
// org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) {
// Let's check for lookup methods here..
if (!this.lookupMethodsChecked.contains(beanName)) {
try {
// Spring 初次缓存beanClass 的相关method。
ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) {
// ...
}
});
}
// 本次的异常打印日志
catch (NoClassDefFoundError err) {
throw new BeanCreationException(beanName, "Failed to introspect bean class [" + beanClass.getName() +
"] for lookup method metadata: could not find class that it depends on", err);
}
this.lookupMethodsChecked.add(beanName);
}
}