乐观锁
概念
在讲解乐观锁之前,我们还是先来分析下问题:
业务并发现象带来的问题:==秒杀==
假如有 100 个商品或者票在出售,为了能保证每个商品或者票只能被一个人购买,如何保证不会出现超买或者重复卖
对于这一类问题,其实有很多的解决方案可以使用
第一个最先想到的就是锁,锁在一台服务器中是可以解决的,但是如果在多台服务器下锁就没有办法控制,比如 12306 有两台服务器在进行卖票,在两台服务器上都添加锁的话,那也有可能会导致在同一时刻有两个线程在进行卖票,还是会出现并发问题
我们接下来介绍的这种方式是针对于小型企业的解决方案,因为数据库本身的性能就是个瓶颈,如果对其并发量超过 2000 以上的就需要考虑其他的解决方案了。
==简单来说,乐观锁主要解决的问题是当要更新一条记录的时候,希望这条记录没有被别人更新。==
实现思路
乐观锁的实现方式:
数据库表中添加 version 列,比如默认值给 1
第一个线程要修改数据之前,取出记录时,获取当前数据库中的 version=1
第二个线程要修改数据之前,取出记录时,获取当前数据库中的 version=1
第一个线程执行更新时,set version = newVersion where version = oldVersion
newVersion = version+1 [2]
oldVersion = version [1]
第二个线程执行更新时,set version = newVersion where version = oldVersion
newVersion = version+1 [2]
oldVersion = version [1]
假如这两个线程都来更新数据,第一个和第二个线程都可能先执行
假如第一个线程先执行更新,会把 version 改为 2,
第二个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据 version 已经为 2,所以第二个线程会修改失败
假如第二个线程先执行更新,会把 version 改为 2,
第一个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据 version 已经为 2,所以第一个线程会修改失败
不管谁先执行都会确保只能有一个线程更新数据,这就是 MP 提供的乐观锁的实现原理分析。
上面所说的步骤具体该如何实现呢?
实现步骤
分析完步骤后,具体的实现步骤如下:
步骤 1:数据库表添加列
列名可以任意,比如使用version,给列设置默认值为1
步骤 2:在模型类中添加对应的属性
根据添加的字段列名,在模型类中添加对应的属性值
@Data//@TableName("tbl_user") 可以不写是因为配置了全局配置public class User { @TableId(type = IdType.ASSIGN_UUID) private String id; private String name; @TableField(value="pwd",select=false) private String password; private Integer age; private String tel; @TableField(exist=false) private Integer online; private Integer deleted; @Version private Integer version;}
复制代码
步骤 3:添加乐观锁的拦截器
@Configurationpublic class MpConfig { @Bean public MybatisPlusInterceptor mpInterceptor() { //1.定义Mp拦截器 MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor(); //2.添加乐观锁拦截器 mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return mpInterceptor; }}
复制代码
步骤 4:执行更新操作
@SpringBootTestclass Mybatisplus03DqlApplicationTests {
@Autowired private UserDao userDao; @Test void testUpdate(){ User user = new User(); user.setId(3L); user.setName("Jock666"); userDao.updateById(user); }}
复制代码
你会发现,这次修改并没有更新 version 字段,原因是没有携带 version 数据。
添加 version 数据
@SpringBootTestclass Mybatisplus03DqlApplicationTests {
@Autowired private UserDao userDao; @Test void testUpdate(){ User user = new User(); user.setId(3L); user.setName("Jock666"); user.setVersion(1); userDao.updateById(user); }}
复制代码
你会发现,我们传递的是 1,MP 会将 1 进行加 1,然后,更新回到数据库表中。
所以要想实现乐观锁,首先第一步应该是拿到表中的 version,然后拿 version 当条件在将 version 加 1 更新回到数据库表中,所以我们在查询的时候,需要对其进行查询
@SpringBootTestclass Mybatisplus03DqlApplicationTests {
@Autowired private UserDao userDao; @Test void testUpdate(){ //1.先通过要修改的数据id将当前数据查询出来 User user = userDao.selectById(3L); //2.将要修改的属性逐一设置进去 user.setName("Jock888"); userDao.updateById(user); }}
复制代码
大概分析完乐观锁的实现步骤以后,我们来模拟一种加锁的情况,看看能不能实现多个人修改同一个数据的时候,只能有一个人修改成功。
@SpringBootTestclass Mybatisplus03DqlApplicationTests {
@Autowired private UserDao userDao; @Test void testUpdate(){ //1.先通过要修改的数据id将当前数据查询出来 User user = userDao.selectById(3L); //version=3 User user2 = userDao.selectById(3L); //version=3 user2.setName("Jock aaa"); userDao.updateById(user2); //version=>4 user.setName("Jock bbb"); userDao.updateById(user); //verion=3?条件还成立吗? }}
复制代码
运行程序,分析结果:
乐观锁就已经实现完成了,如果对于上面的这些步骤记不住咋办呢?
参考官方文档来实现:
https://mp.baomidou.com/guide/interceptor-optimistic-locker.html#optimisticlockerinnerinterceptor
代码生成器
代码生成器原理分析
观察我们之前写的代码,会发现其中也会有很多重复内容,比如:
那我们就想,如果我想做一个 Book 模块的开发,是不是只需要将红色部分的内容全部更换成Book即可,如:
所以我们会发现,做任何模块的开发,对于这段代码,基本上都是对红色部分的调整,所以我们把去掉红色内容的东西称之为==模板==,红色部分称之为==参数==,以后只需要传入不同的参数,就可以根据模板创建出不同模块的 dao 代码。
除了 Dao 可以抽取模块,其实我们常见的类都可以进行抽取,只要他们有公共部分即可。再来看下模型类的模板:
① 可以根据数据库表的表名来填充
② 可以根据用户的配置来生成 ID 生成策略
③到⑨可以根据数据库表字段名称来填充
所以只要我们知道是对哪张表进行代码生成,这些内容我们都可以进行填充。
分析完后,我们会发现,要想完成代码自动生成,我们需要有以下内容:
代码生成器实现
步骤 1:创建一个 Maven 项目
代码 2:导入对应的 jar 包
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.1</version> </parent> <groupId>com.itheima</groupId> <artifactId>mybatisplus_04_generator</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!--spring webmvc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<!--mybatisplus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency>
<!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency>
<!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
<!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency>
<!--代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency>
<!--velocity模板引擎--> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency>
</dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
复制代码
步骤 3:编写引导类
@SpringBootApplicationpublic class Mybatisplus04GeneratorApplication {
public static void main(String[] args) { SpringApplication.run(Mybatisplus04GeneratorApplication.class, args); }
}
复制代码
步骤 4:创建代码生成类
public class CodeGenerator { public static void main(String[] args) { //1.获取代码生成器的对象 AutoGenerator autoGenerator = new AutoGenerator();
//设置数据库相关配置 DataSourceConfig dataSource = new DataSourceConfig(); dataSource.setDriverName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC"); dataSource.setUsername("root"); dataSource.setPassword("root"); autoGenerator.setDataSource(dataSource);
//设置全局配置 GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setOutputDir(System.getProperty("user.dir")+"/mybatisplus_04_generator/src/main/java"); //设置代码生成位置 globalConfig.setOpen(false); //设置生成完毕后是否打开生成代码所在的目录 globalConfig.setAuthor("黑马程序员"); //设置作者 globalConfig.setFileOverride(true); //设置是否覆盖原始生成的文件 globalConfig.setMapperName("%sDao"); //设置数据层接口名,%s为占位符,指代模块名称 globalConfig.setIdType(IdType.ASSIGN_ID); //设置Id生成策略 autoGenerator.setGlobalConfig(globalConfig);
//设置包名相关配置 PackageConfig packageInfo = new PackageConfig(); packageInfo.setParent("com.aaa"); //设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径 packageInfo.setEntity("domain"); //设置实体类包名 packageInfo.setMapper("dao"); //设置数据层包名 autoGenerator.setPackageInfo(packageInfo);
//策略设置 StrategyConfig strategyConfig = new StrategyConfig(); strategyConfig.setInclude("tbl_user"); //设置当前参与生成的表名,参数为可变参数 strategyConfig.setTablePrefix("tbl_"); //设置数据库表的前缀名称,模块名 = 数据库表名 - 前缀名 例如: User = tbl_user - tbl_ strategyConfig.setRestControllerStyle(true); //设置是否启用Rest风格 strategyConfig.setVersionFieldName("version"); //设置乐观锁字段名 strategyConfig.setLogicDeleteFieldName("deleted"); //设置逻辑删除字段名 strategyConfig.setEntityLombokModel(true); //设置是否启用lombok autoGenerator.setStrategy(strategyConfig); //2.执行生成操作 autoGenerator.execute(); }}
复制代码
对于代码生成器中的代码内容,我们可以直接从官方文档中获取代码进行修改,
https://mp.baomidou.com/guide/generator.html
步骤 5:运行程序
运行成功后,会在当前项目中生成很多代码,代码包含controller,service,mapper和entity
至此代码生成器就已经完成工作,我们能快速根据数据库表来创建对应的类,简化我们的代码开发。
MP 中 Service 的 CRUD
回顾我们之前业务层代码的编写,编写接口和对应的实现类:
public interface UserService{ }
@Servicepublic class UserServiceImpl implements UserService{
}
复制代码
接口和实现类有了以后,需要在接口和实现类中声明方法
public interface UserService{ public List<User> findAll();}
@Servicepublic class UserServiceImpl implements UserService{ @Autowired private UserDao userDao; public List<User> findAll(){ return userDao.selectList(null); }}
复制代码
MP 看到上面的代码以后就说这些方法也是比较固定和通用的,那我来帮你抽取下,所以 MP 提供了一个 Service 接口和实现类,分别是:IService和ServiceImpl,后者是对前者的一个具体实现。
以后我们自己写的 Service 就可以进行如下修改:
public interface UserService extends IService<User>{ }
@Servicepublic class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService{
}
复制代码
修改以后的好处是,MP 已经帮我们把业务层的一些基础的增删改查都已经实现了,可以直接进行使用。
编写测试类进行测试:
@SpringBootTestclass Mybatisplus04GeneratorApplicationTests {
private IUserService userService;
@Test void testFindAll() { List<User> list = userService.list(); System.out.println(list); }
}
复制代码
注意:mybatisplus_04_generator 项目中对于 MyBatis 的环境是没有进行配置,如果想要运行,需要提取将配置文件中的内容进行完善后在运行。
思考:在 MP 封装的 Service 层都有哪些方法可以用?
查看官方文档:https://mp.baomidou.com/guide/crud-interface.html,这些提供的方法大家可以参考官方文档进行学习使用,方法的名称可能有些变化,但是方法对应的参数和返回值基本类似。
评论