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. 只回滚子事务,但不回滚父事务。也就是子表不会添加数据,父表会添加数据。在这个例子中表现为业务数据异常。

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

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

相关推荐
LucianaiB几秒前
如何做好一份优秀的技术文档:专业指南与最佳实践
android·java·数据库
面朝大海,春不暖,花不开24 分钟前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
得过且过的勇者y25 分钟前
Java安全点safepoint
java
夜晚回家1 小时前
「Java基本语法」代码格式与注释规范
java·开发语言
斯普信云原生组1 小时前
Docker构建自定义的镜像
java·spring cloud·docker
wangjinjin1801 小时前
使用 IntelliJ IDEA 安装通义灵码(TONGYI Lingma)插件,进行后端 Java Spring Boot 项目的用户用例生成及常见问题处理
java·spring boot·intellij-idea
wtg44521 小时前
使用 Rest-Assured 和 TestNG 进行购物车功能的 API 自动化测试
java
白宇横流学长2 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端
fat house cat_2 小时前
【redis】线程IO模型
java·redis
stein_java3 小时前
springMVC-10验证及国际化
java·spring