乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。
乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。
乐观锁的实现:
- CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
- 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
2️⃣说明 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
MP 中乐观锁的实现
官网描述
OptimisticLockerInnerInterceptor 是 MyBatis Plus 提供的一个内置拦截器,用于实现乐观锁机制。
在并发环境下,乐观锁可以有效防止因并发更新导致的数据不一致问题
当要更新一条记录的时候,希望这条记录没有被别人更新 乐观锁实现方式:
取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败
实现步骤:
a. 配置插件
package com.wdzl.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//Spring boot方式
@Configuration
public class MybatisPlusConfig {
/**
* 新版
*/
@Bean
public MybatisPlusInterceptor mybatisPlusLockInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// OptimisticLockerInnerInterceptor 是 MyBatis Plus 提供的一个内置拦截器
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}
b. 修改表结构
增加一列,用来表示记录的版本。类型必须是数值型的
create table book(
bookid int primary key,
bookname varchar(200),
price float,
pubdate datetime,
author varchar(20),
version int
);
说明:
- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
- 整数类型下
newVersion = oldVersion + 1
newVersion
会回写到entity
中- 仅支持
updateById(id)
与update(entity, wrapper)
方法 - 在
update(entity, wrapper)
方法下,wrapper
不能复用!!!
c. 修改实体类
增加版本属性,并使用注解 @Vesion 标注。
注意:属性名命名没有要求。
@Version
private Integer version;
d. 测试
/**
* UPDATE book SET bookName=?, price=?, pubdate=?, author=?, version=? WHERE bookid=? AND version=?
*/
@Test
public void lock(){
Book book = bookDao.selectById(22);
book.setPrice(book.getPrice()-5);
Book book2 = bookDao.selectById(22);
book2.setPrice(book.getPrice()-60);
bookDao.updateById(book); //按主键修改
//修改失败
bookDao.updateById(book2);
}
}
注意:配置好乐观锁插件和实体类注解后,再次修改,发现 SQL 语句的变化。添加了对于 version 版本字段的添加和修改
UPDATE book SET bookName=?, price=?, pubdate=?, author=?, version=? WHERE bookid=? AND version=?
逻辑删除
逻辑删除是数据库管理中的一种策略,它并不真正从数据库物理层面删除数据,而是通过更新数据表中的某个字段来标记这条记录为“已删除”状态。这样做的好处是可以保留历史数据,避免因误删导致的数据丢失,并且在需要时可以查询到被逻辑删除的记录。
在 MyBatis Plus 中,逻辑删除功能已经内置并提供了便捷的实现方式
官网说明:
说明:
只对自动注入的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
实现步骤:
a) 修改表添加字段
表中添加一个列,用来表示删除状态的列
create table book(
bookid int primary key,
bookname varchar(200),
price float,
pubdate datetime,
author varchar(20),
version int,
deleted int
);
b) 对应修改实体类,增加状态属性。
实体类字段上加上@TableLogic
注解
@TableLogic
private Integer deleted;
c) 提前规定好删除标志的值和正常的值
可以在application.yml 配置状态值
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
d) 测试:
@Test
void logicdel(){
//SELECT bookid,bookName,price,pubdate,author,version,state FROM book WHERE state=0
// List<Book> list = bookDao.selectList(null);
//删除时 UPDATE book SET state=1 WHERE bookid=? AND state=0
bookDao.deleteById(27);
}
注意:在做select 查询,update 修改,删除操作时,执行的 SQL 都会有所变化。结合version来操作
分组
实现要求:
-- 按作者分组,查询每个作者初版的书数量及平均价格
SELECT author, AVG(price) 平均价格,COUNT(*) 数量
FROM book
WHERE state=0
GROUP BY author
HAVING COUNT(*)>2
代码实现:
void group(){
QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
queryWrapper.select("author","count(*) bookCount","avg(price) pavg")
.groupBy("author")
.having("count(*)>2");
List<Map<String, Object>> maps = bookDao.selectMaps(queryWrapper);
for (Map<String, Object> map : maps) {
String author = (String)map.get("author");
Long count = (Long)map.get("bookCount");
Double pavg = (Double)map.get("pavg");
System.out.println(author+"==="+count+"==="+pavg);
}
}
排序、或、链式编程
/**
* order by
*/
public void order(){
QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
// queryWrapper.orderByAsc("price");
queryWrapper
.orderByDesc("price")
.orderByAsc("pubDate")
.le("price",34)
.or()
.gt("pubDate",new Date())
.and(t->t.notIn("bookid",34,45,56))
.gt("price",56);
bookDao.selectList(queryWrapper);