Java – MybatisPlus 常用功能汇总
简介
本章介绍了MybatisPlus的常用功能汇总,为项目中用的比较多的。
环境搭建
pom 引入配置
因为MP已集成了Mybatis的所有功能,因此只要引入MP就可以了,无需再引入Mybatis
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
完整使用引入
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yaml 基础配置
要使用MP连接数据库,我们需要在yaml中配置基础连接参数
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: MySQL123
logging:
level:
com.itheima: debug
pattern:
dateformat: HH:mm:ss
yaml 其它配置
全局设置实体类位置
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po
global-config:
db-config:
id-type: auto # 全局id类型为自增长
全局设置id主键自增配置
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po
global-config:
db-config:
id-type: auto # 全局id类型为自增长
设置mapper XML 声明文件位置
mybatis-plus:
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。
注意:约定好的位置也是以上代码中表示的位置
基础mapper xml 文件格式模板
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">
<select id="queryById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
逻辑删除配置
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为true
- 查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
yaml 配置:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
也可以在单个实体类中的字段中加入 @TableLogic 注解定议该字段为逻辑删除字段
该字段类型为 Integer ,当记录被删除时,会使该字段的值设为 1
常见注解
MP中包含一些比较重要的注解,帮助我们对Pojo实体类做更好的表对接
@TableName
@TableName 注解用于实体类类头,用于声明该实体类对应数据库中的数据表名,如果实体类中与数据库中的表名不一致时,我们可以显式的声明,以便MP对接实体类和数据表的关系。
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 表名 |
schema | String | 否 | "" | schema |
keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
resultMap | String | 否 | "" | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 |
示例:
@TableName("user")
public class User {
private Long id;
private String name;
}
@TableId 主键
@TableId 用于声明该实体类中表示主键ID的成员字段,MP约定我们的实体类中的主键ID名为‘id’.如果我们的实体类中定义主键的名称不叫‘id’,则我们需要显式声名
每个实体类中,都应该有至少一个主键,否则MP会认为所有成员字段都为主键
TableId
注解支持两个属性:
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 表名 |
type | Enum | 否 | IdType.NONE | 指定主键类型 |
IdType
支持的类型有:
值 | 描述 |
---|---|
AUTO | 数据库 ID 自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert 前自行 set 主键值 |
ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) |
分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) | |
32 位 UUID 字符串(please use ASSIGN_UUID) | |
分布式全局唯一 ID 字符串类型(please use ASSIGN_ID) |
示例:
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
}
@TableField
@TableField 是用于声明实体类中非主键以外的字段的一些属性参数
一般情况下我们并不需要给字段添加@TableField
注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量是以
isXXX
命名,按照JavaBean
的规范,MybatisPlus
识别字段时会把is
去除,这就导致与数据库不符。 - 成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField
注解给字段名添加````转义
UpdateWrapper
属性 | 类型 | 必填 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 数据库字段名 |
exist | boolean | 否 | true | 是否为数据库表字段 |
condition | String | 否 | "" | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window) |
update | String | 否 | "" | 字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULL |
insert into table_a( |
||||
updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNORED |
update table_a set column=#{columnProperty} | ||||
whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTY |
where |
||||
fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 |
select | boolean | 否 | true | 是否进行 select 查询 |
keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 |
jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) |
typeHandler | TypeHander | 否 | ||
类型处理器 (该默认值不代表会按照该值生效) | ||||
numericScale | String | 否 | "" | 指定小数点后保留的位数 |
示例:
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
@TableField("isMarried")
private Boolean isMarried;
@TableField("`concat`")
private String concat;
}
字段自动填充
某些字段可能需要每一次update或insert时都全被填充数据,但其数据都是有迹可寻,我们不希望每次的改和增都手动加上该字段的数据,如 create_time 和 update_time 或 create_user 和 update_user 等这类字段,我们希望它能自动填充。
1.在需要自动填充的字段中加入 @TableField(fill=xx) 注解,如下:
public class User {
// 注意!这里需要标记为填充字段
@TableField(fill = FieldFill.INSERT)
private DateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private DateTime updateTime;
}
其值可选以下4种:
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入填充字段
*/
INSERT,
/**
* 更新填充字段
*/
UPDATE,
/**
* 插入和更新填充字段
*/
INSERT_UPDATE
}
2.需要配置这类自动填充字段的数据,上面的步骤只声明了哪些字段具有自动填充功能,但并没有声明这些字段分别填充什么数据,因此需要配置好什么时候会填充什么数据
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
// 或者
this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 或者
this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
// 或者
this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 或者
this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
}
}
开发功能
本节主要讲解MP在代码开发中的常用功能使用。
条件构造器(QueryWrapper)
除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
QueryWrapper
示例:
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2.查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
UpdateWrapper
示例:
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") // SET balance = balance - 200
.in("id", ids); // WHERE id in (1, 2, 4)
// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
// 而是基于UpdateWrapper中的setSQL来更新
userMapper.update(null, wrapper);
}
条件构造器(LambdaQueryWrapper)
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值
。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用
和Lambda
表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
- LambdaQueryWrapper
- LambdaUpdateWrapper
分别对应QueryWrapper和UpdateWrapper
LambdaQueryWrapper
示例:
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
LambdaUpdateWrapper 同理,略
自定义SQL
我们来看一下下图中的代码:
当我们出现类似上图中的修改时,我们发现在代码中出现了不可避免的魔术字符串,在代码的开发中,我们要尽量避免魔术字符串写死在代码中,Wrapper 可以使用 LambdaQueryWrapper ,但这类我们无法通过LambdaQueryWrapper 拼写,在这种情况下,我们都希望把这些转移到Mapper XML 中,我们可以通过传递Wrapper到Mapper XML 中使用。
MP提供了一种方法,可以在Mapper XML 中接收 Wrapper 数据,通过以下方式可接收:
void testCustomWrapper() {
// 1.准备自定义查询条件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
// 2.调用mapper的自定义方法,直接传递Wrapper
userMapper.deductBalanceByIds(200, wrapper);
}
public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
固定写法,需要接收参数 @Param("ew") 即可接收到 Wrapper 并在 XML 中使用,通过 ${ew.customSqlSegment} 获得Wrapper 条件生成的 SQL 代码
Service接口
MP除了对Mapper 和实体类做了代码实现外,还提供了Service的代码实现层,我们可以不需要写Service层的任何代码就可以做到基本的CRUD
save
是新增单个元素saveBatch
是批量新增saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch
是批量的新增或修改
removeById
:根据id删除removeByIds
:根据id批量删除removeByMap
:根据Map中的键值对为条件删除remove(Wrapper<T>)
:根据Wrapper条件删除~~removeBatchByIds~~
:暂不支持
updateById
:根据id修改update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分update(T,Wrapper<T>)
:按照T
内的数据修改与Wrapper
匹配到的数据updateBatchById
:根据id批量修改
getById
:根据id查询1条数据getOne(Wrapper<T>)
:根据Wrapper
查询1条数据getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
listByIds
:根据id批量查询list(Wrapper<T>)
:根据Wrapper条件查询多条数据list()
:查询所有
count()
:统计所有数量count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
基本用法
Service 接口
public interface UserService extends IService<User> {
// 拓展自定义方法
}
Service 实现
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
}
Service 中的 LambdaWrapper 使用
Service中对LambdaQueryWrapper
和LambdaUpdateWrapper
的用法进一步做了简化。我们无需自己通过new
的方式来创建Wrapper
,而是直接调用lambdaQuery
和lambdaUpdate
方法:
void testLambdaQuery() {
// 1.查询1个
User rose = userService.lambdaQuery()
.eq(User::getUsername, "Rose")
.one(); // .one()查询1个
System.out.println("rose = " + rose);
// 2.查询多个
List<User> users = userService.lambdaQuery()
.like(User::getUsername, "o")
.list(); // .list()查询集合
users.forEach(System.out::println);
// 3.count统计
Long count = userService.lambdaQuery()
.like(User::getUsername, "o")
.count(); // .count()则计数
System.out.println("count = " + count);
}
基于Lambda更新:
void testLambdaUpdate() {
userService.lambdaUpdate()
.set(User::getBalance, 800) // set balance = 800
.eq(User::getUsername, "Jack") // where username = "Jack"
.update(); // 执行Update
}
lambdaUpdate()
方法后基于链式编程,可以添加set
条件和where
条件。但最后一定要跟上update()
,否则语句不会执行。
静态工具
对于在某些特殊情况下,A表的Service中可能会引用B表的Service,而B表的Service中也可能会引用A表的Service,这就造成了A表的Service和B表的Service循环引用问题,虽然Spring可以解决循环引用的问题,但对于一些比较复杂的循环引用问题上,Spring可能力不从心,在项目开发中也应避免出现循环引用的问题。
MP提供了静态类 Db,它可以脱离Service中绑定的实体类泛型,若A表和B表的Service有可能被互相引用时,可以取消引用做法,在A表中使用 Db.xxx 调用B表的数据库查询,或在B表中使用 Db.xxx 调用A表的数据库查询
因为Db静态类没有绑定实体泛型类,因此在查询和删除时,需要提供实体类的class
示例:
@Test
void testDbGet() {
User user = Db.getById(1L, User.class);
System.out.println(user);
}
@Test
void testDbList() {
// 利用Db实现复杂条件查询
List<User> list = Db.lambdaQuery(User.class)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000)
.list();
list.forEach(System.out::println);
}
@Test
void testDbUpdate() {
Db.lambdaUpdate(User.class)
.set(User::getBalance, 2000)
.eq(User::getUsername, "Rose");
}
扩展功能
IDEA插件
MybatisX、MybatisPlus
插件代码自动生成
枚举处理器
针对一些字段中使用 int 型来表达多种状态的情况下,如 status 字段,被设计成 1=>正常、2=>冻结、3=>删除 这三种状态时。正常情况下实体类中应当也被设计成 Integer 类型对应字段数据,但对于实体类来说,变量中的1、2、3 这样的数据表达并不灵活。
最好的做法是,我们通过使用枚举类型代替Integer类型存储status字段,示例如下:
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结"),
DELETE(3, "已删除"),
;
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
而实体类中的 status 可改变为下面的类型:
private UserStatus status;
但问题来了,如果把 status 类型变成枚举类型了,这样就和数据库中的int类型不匹配了,且MP无法识别转换。
此时我们可以使用枚举转换器,声明MP需要把枚举类型在存入数据前转换为枚举中的特定数据,配置如下:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
要使MP存入数据库前,把枚举数据转为字段类型,则需要在枚举中声明Value数据,如下:
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
DELETE(3, "已删除"),
;
@EnumValue
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
这样MP会默认把枚举中标记 @EnumValue 的字段作为转换数据存入数据表中。
Json 字段处理器
与上一节的枚举字段处理器类似,当数据表中存储的字段类型是 JSON 时,我们通常在实体类中以 String 类型做为存储,MP提供 Json 字段处理器,实现读取时会把数据表中的Json字段数据转化为Java对象,写数据时把Java对象转为Json字符存入数据表。
配置如下:
/**
* 注意!! 必须开启映射注解,否则读取时将读不到
*
* @TableName(autoResultMap = true)
*
* 以下两种类型处理器,二选一 也可以同时存在
*
* 注意!!选择对应的 JSON 处理器也必须存在对应 JSON 解析依赖包
*/
@TableField(typeHandler = JacksonTypeHandler.class)
// @TableField(typeHandler = FastjsonTypeHandler.class)
private UserInfo Json
配置加密
目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。
1.生成秘钥
首先,我们利用AES工具生成一个随机秘钥,然后对用户名、密码加密:
class MpDemoApplicationTests {
@Test
void contextLoads() {
// 生成 16 位随机 AES 密钥
String randomKey = AES.generateRandomKey();
System.out.println("randomKey = " + randomKey);
// 利用密钥对用户名加密
String username = AES.encrypt("root", randomKey);
System.out.println("username = " + username);
// 利用密钥对用户名加密
String password = AES.encrypt("MySQL123", randomKey);
System.out.println("password = " + password);
}
}
2.yaml配置中使用加密内容
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: mpw:QWWVnk1Oal3258x5rVhaeQ== # 密文要以 mpw:开头
password: mpw:EUFmeH3cNAzdRGdOQcabWg== # 密文要以 mpw:开头
3.运行时提供密钥
在运行时的args中提供以下参数和密钥即可
--mpw.key=xxxxxxxx
分页插件
MP提供插件功能,可使得MP存取数据时被拦截加工处理,而其中MP提供了分页插件拦截器,其原理则是把查询出来的数据被拦截并加工数据页码等数据再被输出,分页插件配置如下:
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
属性类型
属性名 | 类型 | 默认值 | 描述 |
overflow | boolean | FALSE | 溢出总页数后是否进行处理(默认不处理,参见 插件#continuePage 方法) |
maxLimit | Long | 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法) | |
dbType | DbType | 数据库类型(根据类型获取应使用的分页方言,参见 插件#findIDialect 方法) | |
dialect | IDialect | 方言实现类(参见 插件#findIDialect 方法) |
悲观锁乐观锁
乐观锁和悲观锁是在并发编程中用于处理并发访问和资源竞争的两种不同的锁机制!!
悲观锁:上一个未完成的操作对数据表进行锁定,下一个操作等待
悲观锁的基本思想是,在整个数据访问过程中,将共享资源锁定,以确保其他线程或进程不能同时访问和修改该资源。悲观锁的核心思想是"先保护,再修改"。在悲观锁的应用中,线程在访问共享资源之前会获取到锁,并在整个操作过程中保持锁的状态,阻塞其他线程的访问。只有当前线程完成操作后,才会释放锁,让其他线程继续操作资源。这种锁机制可以确保资源独占性和数据的一致性,但是在高并发环境下,悲观锁的效率相对较低。
悲观锁实现方案和技术:
- 锁机制:使用传统的锁机制,如互斥锁(Mutex Lock)或读写锁(Read-Write Lock)来保证对共享资源的独占访问。
- 数据库锁:在数据库层面使用行级锁或表级锁来控制并发访问。
- 信号量(Semaphore):使用信号量来限制对资源的并发访问。
乐观锁:上一个未完成的操作不对数据表进行锁定,下一个操作不停尝试是否有占用情况,一旦没有马上操作
乐观锁的基本思想是,认为并发冲突的概率较低,因此不需要提前加锁,而是在数据更新阶段进行冲突检测和处理。乐观锁的核心思想是"先修改,后校验"。在乐观锁的应用中,线程在读取共享资源时不会加锁,而是记录特定的版本信息。当线程准备更新资源时,会先检查该资源的版本信息是否与之前读取的版本信息一致,如果一致则执行更新操作,否则说明有其他线程修改了该资源,需要进行相应的冲突处理。乐观锁通过避免加锁操作,提高了系统的并发性能和吞吐量,但是在并发冲突较为频繁的情况下,乐观锁会导致较多的冲突处理和重试操作。
乐观锁实现方案和技术:
- 版本号/时间戳:为数据添加一个版本号或时间戳字段,每次更新数据时,比较当前版本号或时间戳与期望值是否一致,若一致则更新成功,否则表示数据已被修改,需要进行冲突处理。
- CAS(Compare-and-Swap):使用原子操作比较当前值与旧值是否一致,若一致则进行更新操作,否则重新尝试。
- 无锁数据结构:采用无锁数据结构,如无锁队列、无锁哈希表等,通过使用原子操作实现并发安全。
总结:悲观锁和乐观锁是两种解决并发数据问题的思路,不是具体技术!!!
Mybatis-Plus 中使用乐观锁
1.每条数据添加一个版本号字段version
2.取出记录时,获取当前 version
3.取出记录时,获取当前 version
4.如果是[证明没有人修改数据], 执行更新, set 数据更新 , version = version+ 1
5.如果 version 不对[证明有人已经修改了],我们现在的其他记录就是失效数据!就更新失败
具体操作:
1.添加版本号更新插件
@Bean
public MybatisPlusInterceptor plusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 增加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 增加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
2.乐观锁字段添加@Version注解
ALTER TABLE USER ADD VERSION INT DEFAULT 1 ; # int 类型 乐观锁字段
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在实体类的 version 字段中加入注解
@Version
private Integer version;
3.正常操作数据库即可,当出现该字段版本号不符合时,数据将不会被修改。
防止全表数据删除和更新
针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除
具体操作:
1.增加防止全表数据删除和更新插件
@Bean
public MybatisPlusInterceptor plusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 增加防止全表操作插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
2.当出现全表更新或全表删除时,则会报出异常:
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error updating database.
(Cause: com.baomidou.mybatisplus.core.exceptionsMybatisPlusException: Prohibition of table update operation
共有 0 条评论