说明
在数据操作的章节中,我们只是讲解了对关系数据库和非关系数据库的简单的数据操作,在实际的业务当中,操作会更加复杂,因此不可避免的会涉及到数据库的事务和数据的缓存。
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 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
|
@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
|
@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
|
@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
|
@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
|
@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
|
@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
|
@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
|
@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
|
@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 应用中使用事务和缓存是比较容易的,增加对应的注解即可实现相关的功能。有了事务和缓存的支持,数据的可靠性和应用的性能可以进一步得到保障,通过以上内容的学习,基本上可以起飞,做业务开发了。