目录
乐观锁( Optimistic Locking )和悲观锁是数据库中的两种并发控制机制。
乐观锁假定数据一般情况下不会发生冲突,因此在读取数据时不会对其加锁,而是在写入时先比较数据版本号(比如时间戳)是否相同,再进行操作。如果版本号相同,则表示该数据没有被其他进程修改,可以进行写操作;如果版本号不同,则表示该数据已经被其他进程修改,写操作会失败,需要重新读取数据进行操作。
乐观锁是为了解决并发过程中数据更新冲突的问题,乐观锁能提高并发过程中的程序吞吐量。
悲观锁则假定数据会发生冲突,因此在读取数据时就会对其加锁,防止其他进程同时修改此数据,直到当前进程操作完成并解锁后,其他进程才能再次操作该数据。
乐观锁和悲观锁的区别主要有以下几点:
加锁时间不同:乐观锁在读取数据时不会对其加锁,而是在写入时进行比较和加锁操作;悲观锁在读取数据时就会对其加锁。
冲突处理方式不同:乐观锁会在写入时进行比较和冲突检测,如果版本号不一致则操作失败,需要重新读取数据;悲观锁则会阻塞其他进程对该数据的访问,直到当前进程完成操作并解锁。
适用场景不同:乐观锁适用于并发量比较小、数据量比较大、操作更多为读取的场景;悲观锁适用于并发量比较大、数据量比较小、操作更多为写入的场景。
总的来说,乐观锁适用于并发冲突较少的场景,可以提高系统的并发性;悲观锁适用于并发冲突较多的场景,可以保证数据的一致性和安全性。
使用数据版本(Version)记录机制实现乐观锁,这是乐观锁最常用的一种实现方式。
如何实现乐观锁?
@Version
注解标记乐观锁,通过 version 字段来保证数据的安全性,当修改数据的时候,会以 version 作为条件,当条件成立的时候才会修改成功。
1)取出记录时,获取当前 version
2)更新时,带上这个 version
3)执行更新时,update tableName set version = oldVersion + 1 where version = oldVersion
4)如果 version 不对,就更新失败
如何使用MyBatis-Plus实现乐观锁?
第一步:给数据库表添加 version 字段,并设置默认值为1
第二步:实体类增加 version 属性,并添加 @Version 注解
@Getter
@Setter
@TableName("t_book")
public class Book implements Serializable {
?
? ?private static final long serialVersionUID = 1L;
? ?
? ?/**
? ? * 书本类型
? ? */
? ?@TableField("booktype")
? ?private String booktype;
?
? ?/**
? ? * 乐观锁
? ? */
? ?@Version
? ?private Integer version;
}
第三步:配置乐观锁插件
@Configuration
public class MyBatisPlusConfig {
? ?@Bean
? ?public MybatisPlusInterceptor mybatisPlusInterceptor() {
? ? ? ?MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
? ? ? ?//注册乐观锁插件
? ? ? ?mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
? ? ? ?return mybatisPlusInterceptor;
? }
}
第四步:测试
@Test
public void demo2(){
? ?//先查询,再修改
? ?Book book = bookMapper.selectById("1685091066406");
? ?book.setBookname("Java编程思想");
? ?book.setPrice(100f);
? ?bookMapper.updateById(book);
}
从控制台的日志信息发现:修改数据时 version 作为条件判断,并且 version 自动完成自增操作,即:version = version+1
。
测试多线程下乐观锁失败:
@Test
public void demo2(){
? ?//线程1:
? ?Book book1 = bookMapper.selectById("1685091066406");
? ?book1.setBookname(".Net之入门");
? ?book1.setPrice(110f);
?
? ?//线程2:(在线程1的修改操作未来得及执行时介入)
? ?Book book2 = bookMapper.selectById("1685091066406");
? ?book2.setBookname("Python之入门");
? ?book2.setPrice(200f);
? ?bookMapper.updateById(book2);
?
? ?//如果没有乐观锁就会覆盖插队线程的值!
? ?bookMapper.updateById(book1);//更新失败
}
MyBatis-Plus
中的逻辑删除(Logical Delete)是在数据库中进行虚拟删除,即实际删除数据时,并不会将数据从数据库中删除,而是通过一个标记来记录其已被删除。这种删除方式称为逻辑删除或软删除。
当我们使用物理删除时,数据将被永久删除,无法恢复。但有些情况下,我们并不希望永久删除数据,比如用户误删除、操作失误等情况,这时逻辑删除就尤为重要。
另外,逻辑删除还可以对应业务层逻辑,将数据状态标志为“已删除”,便于后续查询和统计。同时,逻辑删除还能提高删除操作效率,减少物理删除数据对系统性能的影响。
说明:只对自动注入的 sql 起效。
插入: 不作限制
查找: 追加 where 条件过滤掉已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
更新: 追加 where 条件防止更新到已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
删除: 转变为 更新
例如:
删除: update user set deleted=1 where id = 1 and deleted=0
查找: select id,name,deleted from user where deleted=0
字段类型支持说明:
支持所有数据类型(推荐使用 Integer
,Boolean
,LocalDateTime
)
如果数据库字段使用datetime
,逻辑未删除值和已删除值支持配置为字符串null
,另一个值支持配置为函数来获取值如now()
附录:
逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。
全局配置:
在application.yml
中添加全局逻辑删除配置,如下:
mybatis-plus:
global-config:
? db-config:
? ? logic-delete-field: deleted # 全局逻辑删除的实体字段名
? ? logic-delete-value: 1 ? ? ? # 逻辑已删除值(默认为 1)
? ? logic-not-delete-value: 0 ? # 逻辑未删除值(默认为 0)
在对应的实体类中添加逻辑删除字段,如下:
/**
* 逻辑删除
*/
@TableField("deleted")
private Integer deleted;
这里不需要配置
@TableLogic
注解,必须指定@TableField
并设置对应数据库中的字段名。
局部配置:
请在实体类对应的逻辑删除属性上加入@TableLogic
注解。其中@TableLogic
注解属性介绍如下:
属性名 | 类型 | 说明 |
---|---|---|
value | String | 未逻辑删除的值 |
delval | String | 已逻辑删除的值 |
在实体类上配置逻辑删除字段,如下:
/**
* 逻辑删除,1=删除,0=正常
*/
@TableLogic(value = "0",delval = "1")
@TableField("deleted")
private Integer deleted;
创建junit
测试,使用deleteById
方法进行测试。
@Test
public void demo3(){
? ?//先使用deleteById删除对应的数据
? ?bookMapper.deleteById("1662019679144763393");
? ?//使用查询方法查询数据
? ?//List<Book> books = bookMapper.selectList(null);
? ?//books.forEach(System.out::println);
}
请观察idea
控制台输出结果,会发现执行deleteById
方法后,不再显示delete语句,而是update语句。则表示逻辑删除成功,可查看MySQL
数据表中的结果。
然后,执行selectList(null)
方法,可发现查询语句的where
条件后加入逻辑删除字段deleted=0
的判断处理,表示只查询出未逻辑删除的数据。
QueryWrapper是Mybatis-Plus提供的一个条件构造器,用于快速构建SQL查询语句的条件部分。通过使用QueryWrapper,我们可以方便地进行单表数据的查询、修改、删除等操作。
QueryWrapper的语法类似于Mybatis的XML文件中的where标签,其最终会被转换为SQL语句的条件部分。我们可以通过链式调用的方式,不断添加查询条件,从而构建出复杂的查询条件。
配置分页插件
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
...
//注册分页插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return mybatisPlusInterceptor;
}
}
实现分页
int page=1;
int row=10;
//条件构造器
QueryWrapper<Book> wrapper=new QueryWrapper<>();
//设置条件
//TODO
//设置分页
Page<Book> result = bookMapper.selectPage(new Page<Book>()
.setCurrent(page)
.setSize(row)
, wrapper);
List<Book> records = result.getRecords();
System.out.println("总记录数:"+result.getTotal());
records.forEach(System.out::println);