Mysql乐观锁实战

Mysql乐观锁实战文章首先介绍乐观锁的概念 然后介绍乐观锁的实现原理 最后用一个 springboot 项目演示乐观锁的实现方式 目录什么是乐观锁乐观锁实现原理实战什么是乐观锁在进行数据库操作的时候 乐观锁总是假设查询不会修改数据 因此不会对查询到的数据上锁 只有在真正更新数据的时候再去检测是否有冲突 如果有冲突则更新失败 乐观锁能够提高并发查询的效率 且实现起来非常简单 乐观锁实现原理乐观锁的实现原理是 在表中新增一个 version 字段 每次更新数据库的时候 都去检查 version 字段是否符

文章首先介绍乐观锁的概念,然后介绍乐观锁的实现原理,最后用一个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); 

项目结构如下:

Mysql乐观锁实战

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

# 应用名称 spring.application.name=optimiclock # 应用服务 WEB 访问端口 server.port=8087 spring.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/*.xml mybatis.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 
   
     initUsers(); void process(User user) throws InterruptedException; } 
   

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

updateAccountWallet用于更新存款记录:

 
   
     update account_wallet set user_amount = #{userAmount,jdbcType=DECIMAL}, version = version + 1 where id =#{id,jdbcType=INTEGER} and version = #{version,jdbcType=INTEGER} 
   

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

public List 
   
     initUsers() { List 
    
      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 { @Autowired TestService accountWalletService; @PostMapping(value="/test") @ResponseBody public void test() { List 
   
     users = accountWalletService.initUsers(); //模拟并发 users.parallelStream().forEach(b -> { try { accountWalletService.process(b); } catch (InterruptedException e) { e.printStackTrace(); } }); } } 
   

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

Mysql乐观锁实战

 IDE中打印的消息如下:

Mysql乐观锁实战

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

查看数据库中的记录:

Mysql乐观锁实战

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/176969.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月26日 下午8:36
下一篇 2026年3月26日 下午8:37


相关推荐

  • 计算机的通信协议_计算机通信网络层级

    计算机的通信协议_计算机通信网络层级系列文章目录提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加例如:第一章Python机器学习入门之pandas的使用提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录系列文章目录 前言 一、pandas是什么? 二、使用步骤 1.引入库 2.读入数据 总结前言提示:这里可以添加本文要记录的大概内容:例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的

    2022年8月30日
    5
  • java记录访问时间_在java中记录上次访问时间和上次修改时间?

    java记录访问时间_在java中记录上次访问时间和上次修改时间?首先,让我们关注这些事物的含义.访问–上次读取文件的时间,即上次访问文件数据的时间.修改–上次修改文件(内容已被修改),即文件数据上次修改的时间.更改–文件的元数据的最后一次更改(例如,权限),即上次更改文件状态的时间.编辑.访问时间正在改变.我建议你使用Thread.sleep(100)或其他东西,然后看看这个问题是否仍然存在.如果是这样,罪魁祸首就必须是您正在运行的操作系统,因为J…

    2022年7月8日
    23
  • spring boot slf4j日记记录配置详解

    spring boot slf4j日记记录配置详解Spring Boot 日志操作 全局异常捕获消息处理 日志控制台输出 日志文件记录 nbsp nbsp nbsp nbsp 最好的演示说明 不是上来就贴配置文件和代码 而是 先来一波配置文件的注释 再来一波代码的测试过程 最后再出个技术在项目中的应用效果 这样的循序渐进的方式 才会让读者更加清楚的理解一项技术是如何运用在项目中的 虽然本篇很简单 几乎不用手写什么代码 但是 比起网上其他人写的同类型的文章来说

    2026年3月19日
    2
  • eclipse怎么运行java_使用eclipse编写和运行java程序(基础)「建议收藏」

    eclipse怎么运行java_使用eclipse编写和运行java程序(基础)「建议收藏」1.首先java程序的运行你需要下载和安装JDK,这是java运行的必备环境。2.在桌面上找到eclipes,双击打开。3.在eclipes启动的过程中,会弹出一个窗口,让你填写java工作区的保存目录,在这个目录下会保存你写的所有的源代码文件,建议不要把工作区放在C盘注:修改工作区路径File->SwitchWorkspace4.ecplies启动完成之后,会有一个欢迎页面,这个不…

    2022年7月7日
    24
  • 2026年中国七大智能体平台深度测评:企业级AI Agent选型权威指南

    2026年中国七大智能体平台深度测评:企业级AI Agent选型权威指南

    2026年3月15日
    2
  • python3 typing_python 高级

    python3 typing_python 高级typing介绍Python是一门弱类型的语言,很多时候我们可能不清楚函数参数的类型或者返回值的类型,这样会导致我们在写完代码一段时间后回过头再看代码,忘记了自己写的函数需要传什么类型的参数,返回什

    2022年7月30日
    9

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号