说明
在数据操作的章节中,我们只是讲解了对关系数据库和非关系数据库的简单的数据操作,在实际的业务当中,操作会更加复杂,因此不可避免的会涉及到数据库的事务和数据的缓存。
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 增加用户表(主表)和用户部门表(子表)来演示事务的三个常见:
- 回滚,无论添加主表方法异常,还是添加子表方法异常。在这个例子业务数据正常。
- 只回滚父事务,但不回滚子事务。也就是主表不会添加数据,子表会添加数据。在这个例子中表现为业务数据异常。
- 只回滚子事务,但不回滚父事务。也就是子表不会添加数据,父表会添加数据。在这个例子中表现为业务数据异常。
为了方便说明和查看方便,不同的回滚场景提供的代码会做删减,请注意到仓库获取完整代码。
创建表
SQL
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
Java
@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
Java
/**
* @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
Java
/**
* @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
Java
@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
Java
/**
* @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
Java
/**
* @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
Java
@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
Java
/**
* @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
Java
/**
* @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
Java
/**
* @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 这样才能给缓存序列化。
Java
/**
* @author airhead
*/
@ApiModel
@Data
public class DeptDto implements Serializable {
private Long id;
/** 部门名称 */
private String name;
/** 部门编码 */
private String code;
}
DeptService
保存、更新、删除的时候通过tags清理缓存,获取id和list的时候通过key增加缓存,同时增加tags标签。
Java
/**
* @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 接口时缓存,如果存在缓存时不再从数据库中获取数据。

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