1. 程式人生 > 程式設計 >淺談mybatis 樂觀鎖實現,解決併發問題

淺談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 樂觀鎖實現,解決併發問題就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。