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

相关推荐
yngsqq2 小时前
c# —— StringBuilder 类
java·开发语言
星星点点洲3 小时前
【操作幂等和数据一致性】保障业务在MySQL和COS对象存储的一致
java·mysql
xiaolingting3 小时前
JVM层面的JAVA类和实例(Klass-OOP)
java·jvm·oop·klass·instanceklass·class对象
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
追光少年33224 小时前
迭代器模式
java·迭代器模式
超爱吃士力架5 小时前
MySQL 中的回表是什么?
java·后端·面试
扣丁梦想家5 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式
drebander5 小时前
Maven 构建中的安全性与合规性检查
java·maven
drebander5 小时前
Maven 与 Kubernetes 部署:构建和部署到 Kubernetes 环境中
java·kubernetes·maven
王会举5 小时前
DeepSeek模型集成到java中使用(阿里云版)超简单版
java·阿里云·deepseek