淺談mybatis 樂觀鎖實現(xiàn),解決并發(fā)問題
情景展示:
銀行兩操作員同時操作同一賬戶就是典型的例子。
比如A、B操作員同時讀取一余額為1000元的賬戶,A操作員為該賬戶增加100元,B操作員同時為該賬戶扣除50元,A先提交,B后提交。最后實際賬戶余額為1000-50=950元,但本該為1000+100-50=1050。這就是典型的并發(fā)問題。
樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數(shù)據(jù)版本(Version)記錄機制實現(xiàn)。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個版本標識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表增加一個 “version” 字段來實現(xiàn)。
讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數(shù)據(jù)的版本數(shù)據(jù)與數(shù)據(jù)庫表對應記錄的當前版本信息進行比對,如果提交的數(shù)據(jù)版本號大于數(shù)據(jù)庫表當前版本號,則予以更新,否則認為是過期數(shù)據(jù)。
對于上面修改用戶帳戶信息的例子而言,假設數(shù)據(jù)庫中帳戶信息表中有一個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完成了修改工作,將數(shù)據(jù)版本號加一(version=2),連同帳戶增加后余額(balance=1100),提交至數(shù)據(jù)庫更新,此時由于提交數(shù)據(jù)版本大于數(shù)據(jù)庫記錄當前版本,數(shù)據(jù)被更新,數(shù)據(jù)庫記錄version更新為2。
d、操作員B完成了操作,也將版本號加一(version=2)試圖向數(shù)據(jù)庫提交數(shù)據(jù)(balance=950),但此時比對數(shù)據(jù)庫記錄版本時發(fā)現(xiàn),操作員B提交的數(shù)據(jù)版本號為2,數(shù)據(jù)庫記錄當前版本也為2,不滿足 “提交版本必須大于記錄當前版本才能執(zhí)行更新 “的樂觀鎖策略,因此,操作員B的提交被駁回。
這樣,就避免了操作員B用基于version=1的舊數(shù)據(jù)修改的結(jié)果覆蓋操作員A的操作結(jié)果的可能。
示例代碼:
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:代表開發(fā)支付密碼', check_key varchar(64) comment '平臺進行用戶余額更改時,首先效驗key值,否則無法進行用戶余額更改操作', version int comment '基于mysql樂觀鎖,解決并發(fā)訪問' 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
<!--通過用戶唯一編號,查詢用戶錢包相關(guān)的信息 -->
<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>
<!--用戶錢包數(shù)據(jù)更改 ,通過樂觀鎖(version機制)實現(xiàn) -->
<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;
/**
* 針對業(yè)務系統(tǒng)高并發(fā)-----修改用戶錢包數(shù)據(jù)余額,采用樂觀鎖
*
* @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";
}
}
模擬并發(fā)訪問
package com.settlement.concurrent;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import com.settlement.commons.utils.HttpRequest;
/**
* 模擬用戶的并發(fā)請求,檢測用戶樂觀鎖的性能問題
*
* @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人并發(fā)請求,用戶錢包
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();//計數(shù)器減一 所有線程釋放 并發(fā)訪問。
System.out.println("所有模擬請求結(jié)束 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();//發(fā)送post 請求
}
public void post(){
String result = "";
System.out.println("模擬用戶: "+workerName+" 開始發(fā)送模擬請求 at "+sdf.format(new Date()));
result = HttpRequest.sendPost("http://localhost:8080/Settlement/wallet/walleroptimisticlock.action", "openId="+openId+"&openType="+openType+"&amount="+amount);
System.out.println("操作結(jié)果:"+result);
System.out.println("模擬用戶: "+workerName+" 模擬請求結(jié)束 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
* 創(chuàng)建時間: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);
//數(shù)據(jù)源配置
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);
//執(zhí)行生成
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驅(qū)動 注意版本!-->
<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 樂觀鎖實現(xiàn),解決并發(fā)問題就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring?MVC啟動之HandlerMapping作用及實現(xiàn)詳解
這篇文章主要為大家介紹了Spring?MVC啟動之HandlerMapping作用及實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Java SpringBoot詳解集成以及配置Swagger流程
Swagger 是一個規(guī)范和完整的框架,用于生成、描述、調(diào)用和可視化 RESTful 風格的 Web 服務??傮w目標是使客戶端和文件系統(tǒng)作為服務器以同樣的速度來更新。文件的方法,參數(shù)和模型緊密集成到服務器端的代碼,允許API來始終保持同步2021-10-10
Eclipse設置svn忽略文件或文件夾(svn:ignore)的操作
這篇文章主要介紹了Eclipse設置svn忽略文件或文件夾(svn:ignore)的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01

