文章首先介绍乐观锁的概念,然后介绍乐观锁的实现原理,最后用一个springboot项目演示乐观锁的实现方式。

目录

什么是乐观锁

乐观锁实现原理

实战


什么是乐观锁

在进行数据库操作的时候,乐观锁总是假设查询不会修改数据,因此不会对查询到的数据上锁,只有在真正更新数据的时候再去检测是否有冲突,如果有冲突则更新失败。

有的小伙伴会问:为什么要使用乐观锁?因为在处理并发时,我们经常需要面对竞态条件,即某一方法的返回值取决于运行在线程中操作的交替执行方式(下一节会举栗),这是线程不安全的。乐观锁就是为了保证线程安全性,且提高并发访问的效率。(ps:所谓线程安全性,指的是当多个线程访问某个类时,不管运行环境采用何种调度方式或者线程如何交替执行,这个类始终都能表现出正确的行为)(pps:何为正确的行为:所见即所知we know it when we see it)。

乐观锁实现原理

乐观锁的实现原理是,在表中新增一个version字段,每次更新数据库的时候,都去检查version字段是否符合预期值,如果符合则更新,否则不更新。

举栗:

有一张用户的存款表account,里面有一条小明同学的存款记录,显示账户里有1000块。表结构非常简单:

id user_name     account_num update_time
1 小明    1000 null

现在小明要从自己的账户里取50块钱,如果不使用锁,后台的逻辑会是这样:

a1、先查出小明的存款记录select * from account where user_name="小明",查询出余额为account_num1

a2、存款余额减50后试图更新表update account set account_num=account_num1-50 where user_name="小明"

看起来这样似乎没什么问题,但其实不然。

就在小明操作自己账户的同时,小华也正在给小明还钱,数额100:

b1、先查出小明的存款记录select * from account where user_name="小明",查询出余额为account_num2

b2、存款余额加100后试图更新表update account set account_num=account_num2+100 where user_name="小明"

小明取50,小华还100,理论上小明账户里应该还有1050。

但是因为没有加锁,且以上的a1,a2,b1,b2执行顺序存在随机性,导致结果可能出错。

我们假设执行的顺序是a1,b1,a2,b2,小明和小华查到的余额都是1000,小明成功取了钱,余额设置成了950,但是由于b2最后更新,小明账户的余额会是1100(小明高兴了,银行不乐意);如果执行的顺序是a1,b1,b2,a2,由于a2最后更新,小明的账户余额会是950(小华不高兴了,钱白还了)。

乐观锁正是用来解决上面的并发问题,我们来看看如何解决。

在表中增加一个字段version(名称无所谓):

id user_name account_num update_time version
1 小明 1000 null 1

小明仍然取50块钱:

a1、先查出小明的存款记录select * from account where user_name="小明",查出余额为account_num1,version为version1

a2、存款余额减50后试图更新表update account set account_num=account_num1-50, version = version+1 where user_name="小明" and version=version1

小华存100:

b1、先查出小明的存款记录select * from account where user_name="小明",查出余额为account_num2,version为version2

b2、存款余额加100后试图更新表update account set account_num=account_num2+100, version=version+1 where user_name="小明" and version=version2

注意在更新记录的时候加了一个where条件version,并同时更新version+1。

1、假如执行顺序还是a1,b1,a2,b2,由于a2更新成功后,version+1变为2,那么b2在试图更新的时候,由于where条件中version=1不符合,则该条更新语句不执行,小明的余额变为950,小华还钱失败;

2、同理,假如执行顺序是a1,b1,b2,a2,小明取钱失败,小华还钱成功,余额变为1100;

3、或者执行顺序是a1,a2,b1,b2,那么小明取钱后余额变为950,version变为2,此时小华还钱,更新仍旧成功,余额变为1050,version变为3,两个人都更新成功。

有人可能会问,情况1和情况2中,都有人未更新成功啊,这怎么办。需要声明的是乐观锁的作用是防止并发时产生数据更新不一致的问题,这里其实已经实现了。至于更新失败后怎么处理,那就需要后台去实现一个重试机制(下一节会展示),这就不在乐观锁的功能范围内了。

实战

下面以一个springboot项目为例,看看乐观锁具体是怎么实现的,其中也会提供一种重试机制。

建一张account表:

CREATE TABLE `account_wallet` (`id` int(11) NOT NULL COMMENT '用户钱包主键',`user_open_id` varchar(64) DEFAULT NULL COMMENT '用户中心的用户唯一编号',`user_amount` decimal(10,5) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_time` datetime DEFAULT NULL,`pay_password` varchar(64) DEFAULT NULL,`is_open` int(11) DEFAULT NULL COMMENT '0:代表未开启支付密码,1:代表开发支付密码',`check_key` varchar(64) DEFAULT NULL COMMENT '平台进行用户余额更改时,首先效验key值,否则无法进行用户余额更改操作',`version` int(11) DEFAULT NULL COMMENT '基于mysql乐观锁,解决并发访问',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表中插入一条记录:

INSERT INTO `account_wallet` (`id`, `user_open_id`, `user_amount`, `create_time`, `update_time`, `pay_password`, `is_open`, `check_key`, `version`)
VALUES(1, '1', 1000.00000, NULL, NULL, NULL, NULL, 'haha', 1);

项目结构如下:

配置信息如下:注意修改数据库连接信息。

# 应用名称
spring.application.name=optimiclock# 应用服务 WEB 访问端口
server.port=8087spring.datasource.url=jdbc:mysql://IP:port/demo?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver#实体类别名
mybatis.type-aliases-package=com.example.demo.model
#映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xmlmybatis.configuration.map-underscore-to-camel-case=true

dao层:

我这里使用的mybatis-generator插件直接生成数据库表的mapper,具体使用方法请自行google。

User实体类如下。

@Data
public class User {String openId; //账户String userName; //用户String amount; //存取的数额Boolean openType; //true存 false取
}

service层:

public interface TestService {AccountWallet selectByOpenId(String openId);int updateAccountWallet(AccountWallet record);List<User> initUsers();void process(User user) throws InterruptedException;
}

其中selectByOpenId方法用于查询存款记录:

<select id="selectByOpenId" resultType="com.example.demo.model.AccountWallet">select<include refid="Base_Column_List" />from account_walletwhere user_open_id = #{openId,jdbcType=VARCHAR}</select>

updateAccountWallet用于更新存款记录:

<update id="updateAccountWallet"><![CDATA[update account_wallet set user_amount = #{userAmount,jdbcType=DECIMAL}, version = version + 1 where id =#{id,jdbcType=INTEGER} and version = #{version,jdbcType=INTEGER}]]></update>

initUsers用于初始化用户。我这里为了演示,初始化了10个用户,并随机指定了用户是存或取,金额也随机指定。

public List<User> initUsers() {List<User> res = new ArrayList<>();Random random = new Random();for (int i = 0; i < 10; i++) {User user = new User();user.setUserName(i + "");user.setAmount((String.valueOf(random.nextInt(10) * 5)));//随机指定用户存取的金额user.setOpenId("1");res.add(user);user.setOpenType(random.nextBoolean());//随机指定用户是存还是取}return res;}

process用于模拟存取款操作。

这里介绍下重试机制。首先给用户设定一个重试时长,我这里设定的是35秒,用户在这个时间段内会重复尝试更新数据直到成功或者超时结束。

public void process(User user) throws InterruptedException {//用户开抢时间long startTime = System.currentTimeMillis();Boolean success = false;String message = "";//while时间内会不断尝试更新直到成功while ((startTime + 35000L) >= System.currentTimeMillis()) {AccountWallet accountWallet = selectByOpenId("1");//cash为用户要存入或取出的金额BigDecimal cash = BigDecimal.valueOf(Double.parseDouble(user.getAmount()));cash.doubleValue();cash.floatValue();String add = "+";//+表示存入,-表示取出BigDecimal original = accountWallet.getUserAmount();if (user.getOpenType()) {accountWallet.setUserAmount(accountWallet.getUserAmount().add(cash));} else {add = "-";accountWallet.setUserAmount(accountWallet.getUserAmount().subtract(cash));}//尝试更新数据库int res = updateAccountWallet(accountWallet);if (res == 1) {success = true;message = "成功" + " 基数: " + original + add + cash + " 更新后:" + accountWallet.getUserAmount();break;}//休息后再次尝试更新Thread.sleep(10L);}if (success) {System.out.println(message);} else {System.out.println("失败!");}}

controller层:这里使用了parallelStream的方式模拟并发。

@RestController
@Slf4j
public class TestController {@AutowiredTestService accountWalletService;@PostMapping(value="/test")@ResponseBodypublic void test() {List<User> users = accountWalletService.initUsers();//模拟并发users.parallelStream().forEach(b -> {try {accountWalletService.process(b);} catch (InterruptedException e) {e.printStackTrace();}});}
}

程序将模拟10个用户并发操作数据库中的同一条记录,运行程序并调用test接口:

IDE中打印的消息如下:

从打印的消息可以看出,10个用户的并发访问都成功了,并且都正确的更新了存款余额。

查看数据库中的记录:

能够看到存款余额正确更新,并且version成功更新了10次。

好的,关于乐观锁的介绍就到这里,源码在此lisz112/optimicLock

Mysql乐观锁实战相关推荐

  1. mysql乐观锁处理超卖_通过乐观锁解决库存超卖的问题

    前言 在通过多线程来解决高并发的问题上,线程安全往往是最先需要考虑的问题,其次才是性能.库存超卖问题是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作等.本篇通过M ...

  2. MySQL 乐观锁与悲观锁

    悲观锁 悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁. 悲观锁: ...

  3. mysql乐观锁总结和实践

    2019独角兽企业重金招聘Python工程师标准>>> 上一篇文章<MySQL悲观锁总结和实践>谈到了MySQL悲观锁,但是悲观锁并不是适用于任何场景,它也有它存在的一些 ...

  4. mysql乐观锁总结和实践--转

    原文地址:http://chenzhou123520.iteye.com/blog/1863407 上一篇文章<MySQL悲观锁总结和实践>谈到了MySQL悲观锁,但是悲观锁并不是适用于任 ...

  5. mysql 乐观锁_使用Mysql乐观锁解决并发问题

    使用mysql乐观锁解决并发问题 案例说明 银行两操作员同时操作同一账户.比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后 ...

  6. mysql使用条件限制乐观锁_使用Mysql乐观锁解决并发问题

    使用mysql乐观锁解决并发问题 案例说明 银行两操作员同时操作同一账户.比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后 ...

  7. mysql乐观锁总结和实践 - 青葱岁月 - ITeye博客

    mysql乐观锁总结和实践 - 青葱岁月 - ITeye博客

  8. mysql 乐观锁 命令_MySQL-乐观锁

    悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性.如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这 ...

  9. mysql 乐观锁 脏读_mysql 丢失更新1和2、脏读、不可重复读和幻读 事务隔离级别 悲观锁 乐观锁...

    事务是现代关系型数据库的核心之一.在多个事务并发操作数据库(多线程.网络并发等)的时候,如果没有有效的避免机制,就会出现以下几种问题: ( 第一类丢失更新 A事务撤销时,把已经提交的B事务的更新数据覆 ...

最新文章

  1. 为什么MobileNet的参数这么少
  2. Linux 中 rsync 备份数据使用实例
  3. 移动互联环境下的流程管理
  4. java异步框架feed,Java:IO流里面的BuffeedReader
  5. 计算机操作员考评标准,计算机操作员职业技能鉴定标准.doc
  6. HDU - 5335 Walk Out(bfs+路径输出+贪心)
  7. 什么是ACID理论(二阶段、三阶段提交、TCC)
  8. 对排除VLAN中Trunk配置故障一文的补充
  9. Apache RocketMQ 分享
  10. python编辑器中文字体倒立的_matplotlib的安装和允许中文及几种字体
  11. Python的可变长度参数*和**,传参序列解包,isinstance的使用
  12. echarts3.0版本断点连线的处理
  13. 太牛逼了!这个开源项目,可以把我从视频中移除!
  14. golang Windows下编译linux可执行文件
  15. linux安装ralink驱动程序,Linux 安装 FW150UM/RALINK 无线网卡
  16. 企鹅龙(DRBL)无盘启动+再生龙(clonezilla)网络备份与还原系统
  17. httpwatch professional 破解版v9.4.17
  18. 722 | Crypto Tech Night第五期分享会,参会通道开放中
  19. 最新版Shiro-SpringBoot项目实战笔记
  20. 图计算发展简史(1)

热门文章

  1. 谈谈e话通中wmp的应用
  2. JMM与volatile
  3. 【疑难杂症】360主页锁定解决办法
  4. 2019全球汽车品牌销量排名
  5. C#Chart控件 获取曲线上点的信息
  6. 如何绘制漂亮的深度学习模型图
  7. [惊叹] Flash 9 Alpha 面世宣言!
  8. 【文档神器typora】typora图云picgo + gitee
  9. 微信小程序根据性别判断显示不同的图片
  10. Python预测——多元线性回归