浅谈mybatis 乐观锁实现,解决并发问题

情景展示:

银行两操作员同时操作同一账户就是典型的例子。

比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交。最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050。这就是典型的并发问题。

乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。

读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个version字段,当前值为1;而当前帐户余额字段(balance)为1000元。假设操作员A先更新完,操作员B后更新。

a、操作员A此时将其读出(version=1),并从其帐户余额中增加100(1000+100=1100)。

b、在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并从其帐户余额中扣除50(1000-50=950)。

c、操作员A完成了修改工作,将数据版本号加一(version=2),连同帐户增加后余额(balance=1100),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2。

d、操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=950),但此时比对数据库记录版本时发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足 “提交版本必须大于记录当前版本才能执行更新 “的乐观锁策略,因此,操作员B的提交被驳回。

这样,就避免了操作员B用基于version=1的旧数据修改的结果覆盖操作员A的操作结果的可能。

示例代码:

account建库脚本

drop table if exists account_wallet;

/*==============================================================*/
/* Table: account_wallet          */
/*==============================================================*/
create table account_wallet
(
 id     int not null comment '用户钱包主键',
 user_open_id   varchar(64) comment '用户中心的用户唯一编号',
 user_amount   decimal(10,5),
 create_time   datetime,
 update_time   datetime,
 pay_password   varchar(64),
 is_open    int comment '0:代表未开启支付密码,1:代表开发支付密码',
 check_key   varchar(64) comment '平台进行用户余额更改时,首先效验key值,否则无法进行用户余额更改操作',
 version    int comment '基于mysql乐观锁,解决并发访问'
 primary key (id)
);

dao层

AccountWallet selectByOpenId(String openId);

int updateAccountWallet(AccountWallet record);

service 层

AccountWallet selectByOpenId(String openId);

int updateAccountWallet(AccountWallet record);

serviceImpl层

 public AccountWallet selectByOpenId(String openId) {
 // TODO Auto-generated method stub
 return accountWalletMapper.selectByOpenId(openId);
 }

 public int updateAccountWallet(AccountWallet record) {
 // TODO Auto-generated method stub
 return accountWalletMapper.updateAccountWallet(record);
 }

sql.xml

 <!--通过用户唯一编号,查询用户钱包相关的信息 -->
 <select id="selectByOpenId" resultMap="BaseResultMap" parameterType="java.lang.String">
 select
 <include refid="Base_Column_List" />
 from account_wallet
 where user_open_id = #{openId,jdbcType=VARCHAR}
 </select>
 <!--用户钱包数据更改 ,通过乐观锁(version机制)实现 -->
 <update id="updateAccountWallet" parameterType="com.settlement.model.AccountWallet">
   <![CDATA[
   update account_wallet set user_amount = #{userAmount,jdbcType=DECIMAL}, version = version + 1 where id =#{id,jdbcType=INTEGER} and version = #{version,jdbcType=INTEGER}
   ]]>
 </update>

controller 层

package com.settlement.controller;

import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.settlement.commons.base.BaseController;
import com.settlement.model.AccountWallet;
import com.settlement.service.AccountWalletService;
import com.taobao.api.internal.util.StringUtils;

/**
 * 用户钱包Controller
 *
 * @author zzg
 * @date 2017-02-10
 */

@Controller
@RequestMapping(value = "/wallet")
public class WalletController extends BaseController {

	@Autowired
	private AccountWalletService accountWalletService;

	/**
	 * 针对业务系统高并发-----修改用户钱包数据余额,采用乐观锁
	 *
	 * @return
	 */
	@RequestMapping(value = "/walleroptimisticlock.action", method = RequestMethod.POST)
	@ResponseBody
	public String walleroptimisticlock(HttpServletRequest request) {

		String result = "";

		try {
			String openId = request.getParameter("openId") == null ? null
					: request.getParameter("openId").trim(); // 用户唯一编号
			String openType = request.getParameter("openType") == null ? null
					: request.getParameter("openType").trim(); // 1:代表增加,2:代表减少
			String amount = request.getParameter("amount") == null ? null
					: request.getParameter("amount").trim(); // 金额

			if (StringUtils.isEmpty(openId)) {
				return "openId is null";
			}
			if (StringUtils.isEmpty(openType)) {
				return "openType is null";
			}
			if (StringUtils.isEmpty(amount)) {
				return "amount is null";
			}
			AccountWallet wallet = accountWalletService.selectByOpenId(openId);

			// 用户操作金额
			BigDecimal cash = BigDecimal.valueOf(Double.parseDouble(amount));
			cash.doubleValue();
			cash.floatValue();
			if (Integer.parseInt(openType) == 1) {
				wallet.setUserAmount(wallet.getUserAmount().add(cash));
			} else if (Integer.parseInt(openType) == 2) {
				wallet.setUserAmount(wallet.getUserAmount().subtract(cash));
			}

			int target = accountWalletService.updateAccountWallet(wallet);
			System.out.println("修改用户金额是否:" + (target == 1 ? "成功" : "失败"));

		} catch (Exception e) {
			result = e.getMessage();
			return result;
		}

		return "success";
	}

}

模拟并发访问

package com.settlement.concurrent;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import com.settlement.commons.utils.HttpRequest; 

/**
 * 模拟用户的并发请求,检测用户乐观锁的性能问题
 *
 * @author zzg
 * @date 2017-02-10
 */
public class ConcurrentTest {
	final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	public static void main(String[] args){
		CountDownLatch latch=new CountDownLatch(1);//模拟5人并发请求,用户钱包

		for(int i=0;i<5;i++){//模拟5个用户
			AnalogUser analogUser = new AnalogUser("user"+i,"58899dcd-46b0-4b16-82df-bdfd0d953bfb","1","20.024",latch);
			analogUser.start();
		}
		latch.countDown();//计数器減一 所有线程释放 并发访问。
		System.out.println("所有模拟请求结束 at "+sdf.format(new Date()));
	}
	static class AnalogUser extends Thread{
		String workerName;//模拟用户姓名
		String openId;
		String openType;
		String amount;
		CountDownLatch latch;

		public AnalogUser(String workerName, String openId, String openType, String amount,
				CountDownLatch latch) {
			super();
			this.workerName = workerName;
			this.openId = openId;
			this.openType = openType;
			this.amount = amount;
			this.latch = latch;
		}
		@Override
		public void run() {
			// TODO Auto-generated method stub
			try {
				latch.await(); //一直阻塞当前线程,直到计时器的值为0
	  } catch (InterruptedException e) {
	   e.printStackTrace();
	  }
			post();//发送post 请求
		} 

		public void post(){
			String result = "";
			System.out.println("模拟用户: "+workerName+" 开始发送模拟请求 at "+sdf.format(new Date()));
			result = HttpRequest.sendPost("http://localhost:8080/Settlement/wallet/walleroptimisticlock.action", "openId="+openId+"&openType="+openType+"&amount="+amount);
			System.out.println("操作结果:"+result);
			System.out.println("模拟用户: "+workerName+" 模拟请求结束 at "+sdf.format(new Date())); 

		}
	}
}

补充知识:Mybatis-plus代码生成器,自用版本不带xml

package com.wuyd.mybatispulsdemo;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

/**
 * @author wuyd
 * 创建时间:2019/10/8 11:17
 */
public class CodeGenerator {
 public static void main(String[] args) {
  AutoGenerator mpg = new AutoGenerator();

  //全局配置
  GlobalConfig gc = new GlobalConfig();
  gc.setOutputDir(System.getProperty("user.dir")+"/src/main/java");
  gc.setFileOverride(true);
  //不需要ActiveRecord特性的请改为false
  gc.setActiveRecord(true);
  gc.setSwagger2(true);
  gc.setAuthor("wuyd");

  //自定义文件命名,注意%s 会自动填充表实体属性
  gc.setControllerName("%sController");
  gc.setServiceName("%sService");
  gc.setServiceImplName("%sServiceImpl");
  gc.setEntityName("%sEntity");
  gc.setMapperName("%sMapper");

  mpg.setGlobalConfig(gc);

  //数据源配置
  DataSourceConfig dsc = new DataSourceConfig();
  dsc.setDbType(DbType.MYSQL);
  dsc.setDriverName("com.mysql.jdbc.Driver");
  dsc.setUsername("xxx");
  dsc.setPassword("xxx");
  dsc.setUrl("jdbc:mysql://xxx.xxx.xxx.xxx:3306/xxxxx?useUnicode=true&useSSL=false&characterEncoding=utf8");
  mpg.setDataSource(dsc);

  //策略配置
  StrategyConfig strategy = new StrategyConfig();
  //此处可以修改您的表前缀
  strategy.setTablePrefix(new String[]{});
  //表名生成策略
  strategy.setNaming(NamingStrategy.underline_to_camel);
  strategy.setColumnNaming(NamingStrategy.underline_to_camel);
  //需要生成的表
  strategy.setInclude(new String[]{"knapsacks","knapsacks_kind","knapsacks_prop","knapsacks_recharge_card"});

  strategy.setSuperServiceClass(null);
  strategy.setSuperServiceImplClass(null);

  strategy.setSuperMapperClass(null);
  strategy.setControllerMappingHyphenStyle(true);

  strategy.setEntityLombokModel(true);
  strategy.setEntitySerialVersionUID(true);
  strategy.setEntityTableFieldAnnotationEnable(true);

  mpg.setStrategy(strategy);
  // 配置模板
  TemplateConfig templateConfig = new TemplateConfig();
  templateConfig.setXml(null);
  mpg.setTemplate(templateConfig);

  //包配置
  PackageConfig pc = new PackageConfig();
  pc.setParent("com.wuyd.mybatispulsdemo");
  pc.setController("controller");
  pc.setService("service");
  pc.setServiceImpl("service.impl");
  pc.setMapper("mapper");
  pc.setEntity("entity");
  mpg.setPackageInfo(pc);

  //执行生成
  mpg.execute();
 }
}

pom.xml

  <dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-generator</artifactId>
   <version>3.2.0</version>
  </dependency>
  <!-- ORM 选一款 -->
  <dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>${mybatis-plus-boot-starter.version}</version>
  </dependency>
  <!-- Mysql驱动 注意版本!-->
  <dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>${mysql-connector.version}</version>
  </dependency>
  <dependency>
   <groupId>org.apache.velocity</groupId>
   <artifactId>velocity-engine-core</artifactId>
   <version>2.1</version>
  </dependency>

参考列表

官网代码生成器部分

以上这篇浅谈mybatis 乐观锁实现,解决并发问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 关于MyBatis10种超好用的写法(收藏)

    用来循环容器的标签forEach,查看例子 foreach元素的属性主要有item,index,collection,open,separator,close. item:集合中元素迭代时的别名 index:集合中元素迭代时的索引 open:常用语where语句中,表示以什么开始,比如以'('开始 separator:表示在每次进行迭代时的分隔符 close 常用语where语句中,表示以什么结束 在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的

  • Mybatis中传递多个参数的4种方法总结

    前言 现在大多项目都是使用Mybatis了,但也有些公司使用Hibernate.使用Mybatis最大的特性就是sql需要自己写,而写sql就需要传递多个参数.面对各种复杂的业务场景,传递参数也是一种学问. 下面给大家总结了以下几种多参数传递的方法. 方法1:顺序传参法 #{}里面的数字代表你传入参数的顺序. 这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错. 方法2:@Param注解传参法 #{}里面的名称对应的是注解 @Param括号里面修饰的名称. 这种方法在参数不多的情况还

  • 通过源代码分析Mybatis的功能流程详解

    SQL解析 Mybatis在初始化的时候,会读取xml中的SQL,解析后会生成SqlSource对象,SqlSource对象分为两种. DynamicSqlSource,动态SQL,获取SQL(getBoundSQL方法中)的时候生成参数化SQL. RawSqlSource,原始SQL,创建对象时直接生成参数化SQL. 因为RawSqlSource不会重复去生成参数化SQL,调用的时候直接传入参数并执行,而DynamicSqlSource则是每次执行的时候参数化SQL,所以RawSqlSourc

  • MyBatis使用注解开发实现步骤解析

    mybatis可以使用xml文件编写映射语句,也可以通过注解来编写简单的映射语句,在官方文档中有具体描述.简单的说,因为Java 注解的的表达力和灵活性十分有限,简单的一些语法可以使用注解来编写比较方便,但复杂的语句还是要使用xml文件. 在之前的开发中,我们使用mybatis,需要以下几个步骤: 配置核心文件 创建dao接口,定义方法(如MyBatis工具类) 编写Mapper.xml配置文件,在该Ml文件中编写sql语句 最后把Mapper文件配置在mybatis核心文件中就可以进行测试了

  • 浅谈mybatis 乐观锁实现,解决并发问题

    情景展示: 银行两操作员同时操作同一账户就是典型的例子. 比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交.最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050.这就是典型的并发问题. 乐观锁机制在一定程度上解决了这个问题.乐观锁,大多是基于数据版本(Version)记录机制实现.何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 "

  • 浅谈Mybatis乐观锁插件

    背景:对于数据库的同一条记录,假如有两个人同时对数据进行了修改,然后最终同步到数据库的时候,因为存在着并发,产生的结果是不可预料的.最简单的解决方式就是通过给表的记录加一个version字段,记录在修改的时候需要比较一下version是否匹配,如果匹配就更新,不匹配就直接失败.更新成功则把version+1,也就是所谓的乐观锁.当然这样的逻辑最好能做到对开发人员透明,本插件就是来做这件事情的. 1. 使用方式:在mybatis配置文件中加入如下配置,就完成了. <plugins> <pl

  • 浅谈Yii乐观锁的使用及原理

    本文介绍了Yii乐观锁的使用及原理,自己做个学习笔记,也分享给大家,希望对大家有用处 原理: 数据表中使用一个int类型的字段来存储版本号,即该行记录的版本号.更新数据时,对比版本号是否一致 sql查询代码如下(伪代码) update `test_ver` set `name`="lili" and `ver`=2 where `id`=1 and `ver`=1 即在更新时的where查询条件中,带上之前查询记录时得到的版本号,如果其他线程已经修改了该记录,则版本号势必不会一致,则更

  • 浅谈Mybatis中resultType为hashmap的情况

    现在有一张user表 id ,name,age 我们进行一个简单的查询: <select id="test" resultType="Uer"> select id ,name,age from user </select> 查询完后,怎么去接收这个查询结果呢,通常在这个mapper.xml对应的接口中使用List<User>做为返回值去接收,最后存储的样子就是下面的图 这是一个很简单的单表查询操作,其实这种简单的单表查询操作不需

  • 浅谈Mybatis+mysql 存储Date类型的坑

    场景: 把一个时间字符串转成Date,存进Mysql.时间天数会比实际时间少1天,也可能是小时少了13-14小时 Mysql的时区是CST(使用语句:show VARIABLES LIKE '%time_zone%'; 查) 先放总结: 修改方法: 1. 修改数据库时区 2. 在jdbc.url里加后缀 &serverTimezone=GMT%2B8 3. 代码里设置时区,给SimpleDateFormat.setTimeZone(...) 例外:new Date() 可以直接存为正确时间,其他

  • JPA使用乐观锁应对高并发方式

    目录 JPA使用乐观锁应对高并发 高并发系统的挑战 悲观锁的问题 乐观锁是个好东西 给数据库添加乐观锁 乐观锁 -业务判断 解决高并发 JPA使用乐观锁应对高并发 高并发系统的挑战 在部署分布式系统时,我们通常把多个微服务部署在内网集群中,再用API网关聚合起来对外提供.为了做负载均衡,通常会对每个微服务都启动多个运行实例,通过注册中心去调用. 那么问题来了,因为有多个实例运行都是同一个应用,虽然微服务网关会把每一个请求只转发给一个实例,但当面对高并发时,但它们仍然可能同时操作同一个数据库表,这

  • 浅谈MyBatis原生批量插入的坑与解决方案

    目录 原生批量插入的"坑" 解决方案 分片 Demo 实战 原生批量插入分片实现 总结 前面的文章咱们讲了 MyBatis 批量插入的 3 种方法:循环单次插入.MyBatis Plus 批量插入.MyBatis 原生批量插入,详情请点击<MyBatis 批量插入数据的 3 种方法!> 但之前的文章也有不完美之处,原因在于:使用 「循环单次插入」的性能太低,使用「MyBatis Plus 批量插入」性能还行,但要额外的引入 MyBatis Plus 框架,使用「MyBati

  • 浅谈mybatis中的#和$的区别 以及防止sql注入的方法

    mybatis中的#和$的区别 1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id". 2. $将传入的数据直接显示生成在sql中.如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id,  如果传入的

  • 浅谈mybatis中的#和$的区别

    1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id". 2. $将传入的数据直接显示生成在sql中.如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id, 如果传入的值是id,则解析成的sql为ord

  • 浅谈python 导入模块和解决文件句柄找不到问题

    如果你退出 Python 解释器并重新进入,你做的任何定义(变量和方法)都会丢失.因此,如果你想要编写一些更大的程序,为准备解释器输入使用一个文本编辑器会更好,并以那个文件替代作为输入执行.这就是传说中的脚本 Python 提供了一个方法可以从文件中获取定义,在脚本或者解释器的一个交互式实例中使用.这样的文件被称为模块. 导入模块: python导入模块默认是从sys.path的路径中查找.所以应该把这个模块放在sys.path的值对应的文件夹里.否则就找不到要导入的模块.如果在cmd中或者ID

随机推荐