CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

Web 开发 —— 进阶 事务和缓存

说明

在数据操作的章节中,我们只是讲解了对关系数据库和非关系数据库的简单的数据操作,在实际的业务当中,操作会更加复杂,因此不可避免的会涉及到数据库的事务和数据的缓存。

Solon 通过 Solon data 提供事务的管理和基础的缓存框架,具体的缓存实现还是通过插件的方式来实现的。

事务

Solon 的事务是通过 AOP 的方式实现的,因此在同一个类中的两个方法的调用,事务传播机制是不会生效的。

Solon 通过 Tran 注解来标记事务,主要有 policy (事务传导策略)和 isolation(事务隔离级别)

事务传导策略

传番策略 说明
TranPolicy.required 支持当前事务,如果没有则创建一个新的。这是最常见的选择。也是默认。
TranPolicy.requires_new 新建事务,如果当前存在事务,把当前事务挂起。
TranPolicy.nested 如果当前有事务,则在当前事务内部嵌套一个事务;否则新建事务。
TranPolicy.mandatory 支持当前事务,如果没有事务则报错。
TranPolicy.supports 支持当前事务,如果没有则不使用事务。
TranPolicy.not_supported 以无事务的方式执行,如果当前有事务则将其挂起。
TranPolicy.never 以无事务的方式执行,如果当前有事务则报错。

事务隔离级别

属性 说明
TranIsolation.unspecified 默认(JDBC默认)
TranIsolation.read_uncommitted 脏读:其它事务,可读取未提交数据
TranIsolation.read_committed 只读取提交数据:其它事务,只能读取已提交数据
TranIsolation.repeatable_read 可重复读:保证在同一个事务中多次读取同样数据的结果是一样的
TranIsolation.serializable 可串行化读:要求事务串行化执行,事务只能一个接着一个执行,不能并发执行

示例

虽然 Solon 支持手动的方式提供对事务的操作,但用注解的方式已经足够用,所以这里就不讲解手动使用的方式。如果需要可以查看官网文档 https://solon.noear.org/article/299 。官网也提供了事务的监听器,可能针对性的对事务的事件做些附加的处理。

基于 Web02 增加用户表(主表)和用户部门表(子表)来演示事务的三个常见:

  1. 回滚,无论添加主表方法异常,还是添加子表方法异常。在这个例子业务数据正常。
  2. 只回滚父事务,但不回滚子事务。也就是主表不会添加数据,子表会添加数据。在这个例子中表现为业务数据异常。
  3. 只回滚子事务,但不回滚父事务。也就是子表不会添加数据,父表会添加数据。在这个例子中表现为业务数据异常。

为了方便说明和查看方便,不同的回滚场景提供的代码会做删减,请注意到仓库获取完整代码。

创建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `demo_user` (
`id` int NOT NULL AUTO_INCREMENT,
`nick_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`create_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(255) DEFAULT NULL COMMENT '创建人',
`updater` varchar(255) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

CREATE TABLE `demo_user_dept` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` int DEFAULT NULL COMMENT '用户id',
`dept_id` int DEFAULT NULL COMMENT '部门id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

回滚主子表

回滚主子表的时候,可能出现两种情况,一种是子表方法出现错误,一种是主表方法出现问题。

UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
@Mapping("/user")
@Api("用户管理")
public class UserController {
@Inject private UserService service;

@Mapping("rollbackAll")
@Post
@ApiOperation("回滚全部")
public Boolean rollbackAll(@Body UserDto userDto) {
service.rollbackAll(userDto);

return true;
}

@Mapping("rollbackAll1")
@Post
@ApiOperation("回滚全部1")
public Boolean rollbackAll1(@Body UserDto userDto) {
return service.rollbackAll1(userDto);
}
}
UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author airhead
*/
@Component
@Slf4j
public class UserService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Inject private UserDeptService userDeptService;

@Tran
public void rollbackAll(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());
// 子表方法异常
userDeptService.rollbackUserDept(userDeptDto);
}

@Tran
public Boolean rollbackAll1(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());
userDeptService.addUserDept(userDeptDto);

// 主表方法异常
throw new RuntimeException("不能添加主表");
}
}
UserDeptService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author airhead
*/
@Component
@Slf4j
public class UserDeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Tran
public void addUserDept(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);
}

@Tran
public Boolean rollbackUserDept(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);

throw new RuntimeException("不能添加子表");
}
}
效果

这里就不截图了,可以从日志和看数据库数据,可以看到 user 表和 user_dept 表都不会增加数据。

只回滚主表,但不回滚子表

UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@Mapping("/user")
@Api("用户管理")
public class UserController {
@Inject private UserService service;

@Mapping("rollbackMaster")
@Post
@ApiOperation("回滚主表")
public Boolean rollbackMaster(@Body UserDto userDto) {
service.rollbackMaster(userDto);
return true;
}
}
UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author airhead
*/
@Component
@Slf4j
public class UserService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Inject private UserDeptService userDeptService;

@Tran
public void rollbackMaster(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());

userDeptService.addUserDept1(userDeptDto);

throw new RuntimeException("不能添加主表");
}
}
UserDeptService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author airhead
*/
@Component
@Slf4j
public class UserDeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Tran(policy = TranPolicy.requires_new)
public void addUserDept1(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);
}
}
效果

这里就不截图了,可以从日志和看数据库数据,可以看到 user 表不会增加数据,但 user_dept 表会增加数据。

只回滚子表,但不回滚主表

UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@Mapping("/user")
@Api("用户管理")
public class UserController {
@Inject private UserService service;

@Mapping("rollbackSub")
@Post
@ApiOperation("回滚子表")
public Boolean rollbackSub(@Body UserDto userDto) {
service.rollbackSub(userDto);

return true;
}
}
UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author airhead
*/
@Component
@Slf4j
public class UserService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Inject private UserDeptService userDeptService;

@Tran
public void rollbackSub(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());

try {
userDeptService.rollbackUserDept1(userDeptDto);
} catch (Exception ignore) {
// 忽略子表的异常
}
}
}
UserDeptService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author airhead
*/
@Component
@Slf4j
public class UserDeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Tran(policy = TranPolicy.nested)
public Boolean rollbackUserDept1(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);

throw new RuntimeException("不能添加子表");
}
}
效果

这里就不截图了,可以从日志和看数据库数据,可以看到 user 表会添加数据,但 user_dept 表不会添加数据。

缓存

Solon 通过 solon-data 插件提供缓存的基础框架,然后通过不同的缓存插件实现具体的缓存逻辑,使得应用层可以快速的切换缓存的存储或者实现多级缓存。

Solon 的缓存框架使用 key(唯一标识) 和 tags(标签)两个维度来管理缓存。

  • key :缓存唯一标识,没有指定时会自动生成。无论自己指定还是自动生成都需要注意避免 key 重复。
  • tags:缓存标签,可以用于标签的批量操作,通常为批量删除。

注解说明

@Cache 注解:

属性 说明
service() 缓存服务
seconds() 缓存时间
key() 缓存唯一标识,支持字符串模版
tags() 缓存标签,多个以逗号隔开(为当前缓存块添加标签,用于清除)

@CachePut 注解:

属性 说明
service() 缓存服务
seconds() 缓存时间
key() 缓存唯一标识,支持字符串模版
tags() 缓存标签,多个以逗号隔开(为当前缓存块添加标签,用于清除)

@CacheRemove 注解:

属性 说明
service() 缓存服务
keys() 缓存唯一标识,多个以逗号隔开,支持字符串模版
tags() 缓存标签,多个以逗号隔开(方便清除一批key)

示例

我们继续通过部门管理的例子来演示数据的缓存。

RedisConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author airhead
*/
@Configuration
public class RedisConfig {
@Bean(typed = true)
public CacheService defaultCache(@Inject RedisClient redisClient) {
RedisCacheService cacheService = new RedisCacheService(redisClient, 30);
cacheService.enableMd5key(false);
return cacheService;
}

@Bean(typed = true)
public RedisClient defaultClient(
@Inject("${demo.redis}") RedisClientSupplier redisClientSupplier) {
return redisClientSupplier.get();
}
}

DeptDto

需要实现 Serializable 这样才能给缓存序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author airhead
*/
@ApiModel
@Data
public class DeptDto implements Serializable {
private Long id;

/** 部门名称 */
private String name;

/** 部门编码 */
private String code;
}

DeptService

保存、更新、删除的时候通过tags清理缓存,获取id和list的时候通过key增加缓存,同时增加tags标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @author airhead
*/
@Component
@Slf4j
public class DeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@CachePut(key = "dept:list", tags = "dept")
public List<DeptDto> list() {
log.info("search list from db");

return entityQuery.queryable(DeptEntity.class).select(DeptDto.class).toList();
}

@CacheRemove(tags = "dept")
public Boolean add(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);

return entityQuery.insertable(dept).executeRows(true) > 0L;
}

@CacheRemove(tags = "dept")
public Boolean update(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);
return entityQuery.updatable(dept).executeRows() > 0L;
}

@CachePut(key = "dept:${id}", tags = "dept")
public DeptDto get(Long id) {
log.info("search from db by id");

return entityQuery
.queryable(DeptEntity.class)
.whereById(id)
.select(DeptDto.class)
.firstOrNull();
}

@CacheRemove(tags = "dept")
public Boolean delete(Long id) {
return entityQuery
.getEasyQueryClient()
.deletable(DeptEntity.class)
.allowDeleteStatement(true)
.whereById(id)
.executeRows()
> 0L;
}
}

效果

调用 list 或 get 接口时缓存,如果存在缓存时不再从数据库中获取数据。

img

小结

要在 Solon 应用中使用事务和缓存是比较容易的,增加对应的注解即可实现相关的功能。有了事务和缓存的支持,数据的可靠性和应用的性能可以进一步得到保障,通过以上内容的学习,基本上可以起飞,做业务开发了。

欢迎关注我的其它发布渠道