SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁

SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁

一、概述

之前有两篇《SpringBoot入门建站全系列(三)Mybatis操作数据库》《SpringBoot入门建站全系列(四)Mybatis使用进阶篇:动态SQL与分页》介绍了Springboot如何结合Mybatis进行数据库访问操作。这一篇介绍下springboot环境下Mybatis如何进行乐观锁、悲观锁的使用。

悲观锁和乐观锁的概念:

  • 悲观锁:就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
  •  
  • 乐观锁:不上锁,读取的时候带版本号,写入的时候带着这个版本号,如果不一致就失败,乐观锁适用于多读的应用类型,因为写多的时候会经常失败。

代码可以在Springboot组件化构建https://www.pomit.cn/java/spring/springboot.html中的MybatisLock组件中查看,并下载。

首发地址:

  品茗IT-同步发布

二、配置

本文假设你已经引入spring-boot-starter-web。已经是个SpringBoot项目了,如果不会搭建,可以打开这篇文章看一看《SpringBoot入门建站全系列(一)项目建立》

2.1 Maven依赖

需要引入spring-boot-starter-data-jpa,这里要访问数据库,所以要依赖数据库相关jar包。

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
		</dependency>

2.2 配置文件

在application.properties 中需要添加下面的配置:

spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
spring.datasource.dbcp2.max-wait-millis=60000
spring.datasource.dbcp2.min-idle=20
spring.datasource.dbcp2.initial-size=2
spring.datasource.dbcp2.validation-query=SELECT 1
spring.datasource.dbcp2.connection-properties=characterEncoding=utf8
spring.datasource.dbcp2.validation-query=SELECT 1
spring.datasource.dbcp2.test-while-idle=true
spring.datasource.dbcp2.test-on-borrow=true
spring.datasource.dbcp2.test-on-return=false

spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/boot?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=cff
spring.datasource.password=123456

mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

这里面,包含了数据库连接信息、数据源的连接池配置信息、mybatis配置信息。

  • spring.datasource.dbcp2是配置dbcp2的连接池信息;
  • spring.datasource.type指明数据源的类型;
  • 最上面的spring.datasource.xxx指明数据库连接池信息;
  • mybatis.configuration.log-impl指明mybatis的日志打印方式

三、悲观锁

悲观锁在数据库的访问中使用,表现为:前一次请求没执行完,后面一个请求就一直在等待。

3.1 Dao层

数据库要实现悲观锁,就是将sql语句带上for update即可。 for update 是行锁

所在mybatis的查询sql加上for update,就实现了对当前记录的锁定,就实现了悲观锁。

UserInfoDao :

package com.cff.springbootwork.mybatislock.dao;

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 com.cff.springbootwork.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 com.cff.springbootwork.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 com.cff.springbootwork.mybatislock.dao;

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 com.cff.springbootwork.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 com.cff.springbootwork.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-博客《SpringBoot入门建站全系列(二十一)Mybatis使用乐观锁与悲观锁》进行查看

品茗IT-博客专题:https://www.pomit.cn/lecture.html汇总了Spring专题Springboot专题SpringCloud专题web基础配置专题。

快速构建项目

Spring组件化构建

SpringBoot组件化构建

SpringCloud服务化构建

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

HFish:企业安全测试主动攻击型蜜罐钓鱼框架系统

HFish是一款基于 Golang 开发的跨平台多功能主动攻击型蜜罐钓鱼平台框架系统,为了企业安全防护测试做出了精心的打造。

24130
来自专栏理想二旬不止

JDBC:【第一篇:入门知识介绍、工具类、PreparedStatment详解】

想要通过Java操作不同的数据库,应该根据数据库的不同而执行特定的API,而出于简化的想法,Sun公司,定义了一套面向所有关系型数据库的 API 即 JDBC ...

11020
来自专栏理想二旬不止

JDBC:【第二篇:控制事务】

原子性(atomicity)、一致性(consistency)、隔离性 (isolation)和持久性(durability)

12040
来自专栏一Li小麦

node服务及前端应用部署

本文案例取自在笔者腾讯云服务器上的实践。上线部署在大公司里其实是专人操作的,一个产品从构思到发布,许许多多的坑要踩。

31030
来自专栏白安全组

MySQL手工暴库语句

本期给大家整理了一下手工的爆库语句,虽然没有sqlmap那么好,但是在特定的情况下还是很有用,大家可以收藏作为一个笔记使用。

13020
来自专栏理想二旬不止

MySQL:【第一篇 入门及基本对象讲解】

如果在我们的数据极为庞大的情况下,我们需要查询其中的一些数据,例如,查询Eminem的国籍,我们一般会使用遍历,但是毫无疑问,这个响应时间会变得极其缓慢,但是使...

13030
来自专栏理想二旬不止

JDBC:【第三篇:数据库数据库连接池 & JDBCTemplate】

简单理解:就是一个存放数据库连接的容器,当用户需要访问数据库的时候,容器提供连接对象给用户用,之后用户将连接对象归还给容器

16320
来自专栏理想二旬不止

MySQL:【第四篇 数据库设计及备份】

10930
来自专栏白安全组

批量入侵SQL注入网站

这里有个输入域名的地方(就是你想拿哪个站点的网址)我们点批量扫描注入点,导入前面扫描的站点,然后批量查询

35650
来自专栏happyJared

MySQL 字符集、校对规则及索引

字符集指的是一种从二进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规则。

12330

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励