Spring整合Mybatis的乐观锁与悲观锁详情
一、概述
前面一篇《Spring和Mybatis整合详解》介绍了Spring如何结合mybatis进行数据库访问操作。上一篇《Spring和SpringDataJpa整合的乐观锁与悲观锁详情》也介绍了Spring-data-jpa如何进行乐观锁和悲观锁的使用。这一篇介绍下springmvc环境下Mybatis如何进行乐观锁、悲观锁的使用。
悲观锁和乐观锁的概念:
悲观锁:就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:不上锁,读取的时候带版本号,写入的时候带着这个版本号,如果不一致就失败,乐观锁适用于多读的应用类型,因为写多的时候会经常失败。
代码可以在Spring组件化构建https://www.pomit.cn/java/spring/spring.html中的MybatisLock组件中查看,并下载。
首发地址:
品茗IT提供在线支持:
如果大家正在寻找一个java的学习环境,或者在开发中遇到困难,可以加入我们的java学习圈,点击即可加入,共同学习,节约学习时间,减少很多在学习中遇到的难题。
二、环境配置
本文假设你已经引入Spring必备的一切了,已经是个Spring项目了,如果不会搭建,可以打开这篇文章看一看《Spring和Spring Mvc 5整合详解》。
2.1 maven依赖
和前面的《Spring和Mybatis整合详解》的配置一样,
使用Mybatis需要引入mybatis和mybatis-spring,已经数据源和connector。
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.pomit</groupId>
<artifactId>SpringWork</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>MybatisLock</artifactId>
<packaging>jar</packaging>
<name>MybatisLock</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
<build>
<finalName>MybatisLock</finalName>
</build>
</project>
父模块可以在https://www.pomit.cn/spring/SpringWork/pom.xml获取。
2.2 Spring配置
需要配置数据源、jdbcTemplate、sqlSessionFactory、transactionManager和MapperScannerConfigurer。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="annotationPropertyConfigurerMybatisLock"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1" />
<property name="ignoreUnresolvablePlaceholders" value="true" />
<property name="locations">
<list>
<value>classpath:mybatis.properties</value>
</list>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${db.dirverClass}"></property>
<property name="url" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
<property name="initialSize" value="1" />
<property name="minIdle" value="1" />
<property name="maxTotal" value="20" />
<property name="validationQuery" value="SELECT 1" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- mybatis -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml" />
<!-- 如果用xml方式,而不是用注解去写sql,这地方要注明xml文件的路径 -->
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.pomit.springwork.mybatislock.mapper" />
</bean>
</beans>
这里面,需要注意的是:
dataSource,这里用的是dbcp2数据源。
sqlSessionFactory,是mybatis的连接信息配置。
MapperScannerConfigurer,指明mapper的路径,要使用Mybatis的注解sql必须指明。
transactionManager,事务处理器。
tx:annotation-driven:开启事务注解。
mybatis.properties中存放数据库的地址端口等连接信息。
mybatis.properties:
db.url=jdbc:mysql://127.0.0.1:3306/boot?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
db.username=cff
db.password=123456
db.dirverClass=com.mysql.cj.jdbc.Driver
mybatis/mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
</configuration>
这里我只配置了日志打印功能。
三、悲观锁
悲观锁在数据库的访问中使用,表现为:前一次请求没执行完,后面一个请求就一直在等待。
3.1 Dao层
数据库要实现悲观锁,就是将sql语句带上for update即可。 for update 是行锁
所在mybatis的查询sql加上for update,就实现了对当前记录的锁定,就实现了悲观锁。
UserInfoDao :
package cn.pomit.springwork.mybatislock.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import cn.pomit.springwork.mybatislock.domain.UserInfo;
@Mapper
public interface UserInfoDao {
@Select({
"<script>",
"SELECT ",
"user_name as userName,passwd,name,mobile,valid, user_type as userType, version as version",
"FROM user_info_test",
"WHERE user_name = #{userName,jdbcType=VARCHAR} for update",
"</script>"})
UserInfo findByUserNameForUpdate(@Param("userName") String userName);
@Update({
"<script>",
" update user_info_test set",
" name = #{name, jdbcType=VARCHAR}, mobile = #{mobile, jdbcType=VARCHAR},version=version+1 ",
" where user_name=#{userName}",
"</script>"
})
int update(UserInfo userInfo);
@Insert({
"<script>",
"INSERT INTO user_info_test",
"( user_name,",
"name ,",
"mobile,",
"passwd,",
"version",
") ",
" values ",
"( #{userName},",
"#{name},",
"#{mobile},",
"#{passwd},",
"#{version}",
" ) ",
"</script>"
})
int save(UserInfo entity);
}
这里,findByUserNameForUpdate的sql中加上了for update。update就是普通的更新而已。
3.2 Service层
更新数据库前,先调用findByUserNameForUpdate方法,使上面的配置的悲观锁锁定表记录,然后再更新。
UserInfoService :
package cn.pomit.springwork.mybatislock.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.mapper.UserInfoDao;
@Service
public class UserInfoService {
@Autowired
UserInfoDao userInfoDao;
public void save(UserInfo entity) {
entity.setVersion(0);
userInfoDao.save(entity);
}
@Transactional
public UserInfo getUserInfoByUserNamePessimistic(String userName) {
return userInfoDao.findByUserNameForUpdate(userName);
}
@Transactional
public void updateWithTimePessimistic(UserInfo entity, int time) throws InterruptedException {
UserInfo userInfo = userInfoDao.findByUserNameForUpdate(entity.getUserName());
if (userInfo == null)
return;
if (!StringUtils.isEmpty(entity.getMobile())) {
userInfo.setMobile(entity.getMobile());
}
if (!StringUtils.isEmpty(entity.getName())) {
userInfo.setName(entity.getName());
}
Thread.sleep(time * 1000L);
userInfoDao.update(userInfo);
}
@Transactional
public void updatePessimistic(UserInfo entity) {
UserInfo userInfo = userInfoDao.findByUserNameForUpdate(entity.getUserName());
if (userInfo == null)
return;
if (!StringUtils.isEmpty(entity.getMobile())) {
userInfo.setMobile(entity.getMobile());
}
if (!StringUtils.isEmpty(entity.getName())) {
userInfo.setName(entity.getName());
}
userInfoDao.update(userInfo);
}
}
测试中,我们在update方法中sleep几秒,其他线程的update将一直等待。
3.3 测试Web层
可以先调用/update/{time}接口,延迟执行,然后马上调用/update接口,会发现,/update接口一直在等待/update/{time}接口执行完成。
MybatisPessLockRest :
package cn.pomit.springwork.mybatislock.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.service.UserInfoService;
/**
* 测试乐观锁
*
* @author fufei
*
*/
@RestController
@RequestMapping("/mybatispesslock")
public class MybatisPessLockRest {
@Autowired
UserInfoService userInfoService;
@RequestMapping(value = "/detail/{name}", method = { RequestMethod.GET })
public UserInfo detail(@PathVariable("name") String name) {
return userInfoService.getUserInfoByUserNamePessimistic(name);
}
@RequestMapping(value = "/save")
public String save(@RequestBody UserInfo userInfo) throws InterruptedException {
userInfoService.save(userInfo);
return "0000";
}
@RequestMapping(value = "/update/{time}")
public String update(@RequestBody UserInfo userInfo, @PathVariable("time") int time) throws InterruptedException {
userInfoService.updateWithTimePessimistic(userInfo, time);
return "0000";
}
@RequestMapping(value = "/update")
public String update(@RequestBody UserInfo userInfo) throws InterruptedException {
userInfoService.updatePessimistic(userInfo);
return "0000";
}
}
四、乐观锁
数据库访问dao层还是3.1那个UserInfoDao。
4.1 Dao层
UserInfoDao更新时,需要携带version字段进行更新:and version = #{version}。如果version不一致,是不会更新成功的,这时候,我们的select查询是不能带锁的。
UserInfoDao :
package cn.pomit.springwork.mybatislock.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import cn.pomit.springwork.mybatislock.domain.UserInfo;
@Mapper
public interface UserInfoDao {
@Select({
"<script>",
"SELECT ",
"user_name as userName,passwd,name,mobile,valid, user_type as userType, version as version",
"FROM user_info_test",
"WHERE user_name = #{userName,jdbcType=VARCHAR}",
"</script>"})
UserInfo findByUserName(@Param("userName") String userName);
@Update({
"<script>",
" update user_info_test set",
" name = #{name, jdbcType=VARCHAR}, mobile = #{mobile, jdbcType=VARCHAR},version=version+1 ",
" where user_name=#{userName} and version = #{version}",
"</script>"
})
int updateWithVersion(UserInfo userInfo);
@Insert({
"<script>",
"INSERT INTO user_info_test",
"( user_name,",
"name ,",
"mobile,",
"passwd,",
"version",
") ",
" values ",
"( #{userName},",
"#{name},",
"#{mobile},",
"#{passwd},",
"#{version}",
" ) ",
"</script>"
})
int save(UserInfo entity);
}
4.2 Service层
service层我们做一下简单的调整。更新数据库前,先调用findByUserName方法,查询出当前的版本号,然后再更新。
UserInfoService :
package cn.pomit.springwork.mybatislock.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.mapper.UserInfoDao;
@Service
public class UserInfoService {
@Autowired
UserInfoDao userInfoDao;
public UserInfo getUserInfoByUserName(String userName){
return userInfoDao.findByUserName(userName);
}
public void save(UserInfo entity) {
entity.setVersion(0);
userInfoDao.save(entity);
}
@Transactional
public void updateWithTimeOptimistic(UserInfo entity, int time) throws Exception {
UserInfo userInfo = userInfoDao.findByUserName(entity.getUserName());
if (userInfo == null)
return;
if (!StringUtils.isEmpty(entity.getMobile())) {
userInfo.setMobile(entity.getMobile());
}
if (!StringUtils.isEmpty(entity.getName())) {
userInfo.setName(entity.getName());
}
Thread.sleep(time * 1000L);
int ret = userInfoDao.updateWithVersion(userInfo);
if(ret < 1)throw new Exception("乐观锁导致保存失败");
}
@Transactional
public void updateOptimistic(UserInfo entity) throws Exception {
UserInfo userInfo = userInfoDao.findByUserName(entity.getUserName());
if (userInfo == null)
return;
if (!StringUtils.isEmpty(entity.getMobile())) {
userInfo.setMobile(entity.getMobile());
}
if (!StringUtils.isEmpty(entity.getName())) {
userInfo.setName(entity.getName());
}
int ret = userInfoDao.updateWithVersion(userInfo);
if(ret < 1)throw new Exception("乐观锁导致保存失败");
}
}
4.2 测试Web层
可以先调用/update/{time}接口,延迟执行,然后马上调用/update接口,会发现,/update接口不会等待/update/{time}接口执行完成,读取完版本号能够成功更新数据,但是/update/{time}接口等待足够时间以后,更新的时候会失败,因为它的版本和数据库的已经不一致了。
注意: 这里更新失败不会抛异常,但是返回值会是0,即更新不成功,需要自行判断。jpa的乐观锁可以抛出异常,手动catch到再自行处理。
MybatisOptiLockRest :
package cn.pomit.springwork.mybatislock.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import cn.pomit.springwork.mybatislock.domain.UserInfo;
import cn.pomit.springwork.mybatislock.service.UserInfoService;
/**
* 测试乐观锁
* @author fufei
*
*/
@RestController
@RequestMapping("/mybatislock")
public class MybatisOptiLockRest {
@Autowired
UserInfoService userInfoService;
@RequestMapping(value = "/detail/{name}", method = { RequestMethod.GET })
public UserInfo detail(@PathVariable("name") String name) {
return userInfoService.getUserInfoByUserName(name);
}
@RequestMapping(value = "/save")
public String save(@RequestBody UserInfo userInfo) throws InterruptedException {
userInfoService.save(userInfo);
return "0000";
}
@RequestMapping(value = "/update/{time}")
public String update(@RequestBody UserInfo userInfo, @PathVariable("time") int time) throws Exception {
userInfoService.updateWithTimeOptimistic(userInfo, time);
return "0000";
}
@RequestMapping(value = "/update")
public String update(@RequestBody UserInfo userInfo) throws Exception {
userInfoService.updateOptimistic(userInfo);
return "0000";
}
}
五、过程中用到的完整实体和Service
UserInfo:
UserInfoService :
UserInfoDao:
详细完整的实体和逻辑,可以访问品茗IT-博客《Spring整合Mybatis的乐观锁与悲观锁详情》进行查看
品茗IT-博客专题:https://www.pomit.cn/lecture.html汇总了Spring专题、Springboot专题、SpringCloud专题、web基础配置专题。
快速构建项目
喜欢这篇文章么,喜欢就加入我们一起讨论Spring技术吧!