百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

互联网大厂面试系列-如何基于数据库实现分布式锁?

mhr18 2024-12-06 16:20 14 浏览 0 评论

在前面的分享中,我们介绍了关于分布式锁相关的内容并且介绍了关于分布式锁的实现方式等。其中比较流行或者说是比较常用的就是基于数据库级别的乐观锁、悲观锁,基于Redis原子操作实现的分布式锁,以及基于Zookeeper的互斥排它锁。

下面我们先来看看关于数据库级别的分布式锁怎么去实现。

乐观锁简介

在个人理解中乐观锁的实现方式其实比较“佛系”的一种实现方式,在实现过程中总是觉得不会发生并发问题,所以每次从数据库中获取数据的时候总是会认为不会有其他的线程对数据进行修改,所以直接获取数据进行修改即可;但是修改完成之后对于更新的时候的操作,就会判断到有没有其他线程对数据进行修改了,所以在其中引入了一个版本号的机制。

所谓的版本号机制,就是当前线程取出数据的时候,会顺便带着一个版本号,这个版本号在最后更新记录的时候就会作为判断的条件,当数据操作成功之后,将版本号进行加一操作。这个时候,其他线程如果对改值进行更新的时候就会发现其对应的版本号不是自己获取到的那个版本号,就会检查数据的修改操作。从而避免了在多线程并场景下的数据不一致现象。

如图所示,展示了数据库实现分布式乐观锁的运行原理,下面我们就来看看如何实践?

数据库分布式乐观锁实现

这里我们以转账系统为例来完成分布式乐观锁的实战操作。如下图所示,在提现过程中用户从余额中进行提现,但是由于网络的原因,用户发起了两次提现请求,分别交给了线程一和线程二操作,这个时候就相当于并发去操作余额了。那么当请求到达后端接口的时候,就会出现并发修改余额的操作。

当然在正常情况下,完全可以满足条件。但是如果在恶意操作的情况下,导致出现了同一时刻产生了大量并发线程。但是由于后端的每个接口都在处理请求,就会导致先获取到余额的线程对余额进行了修改,而其他线程同时获取到了相关的余额值,最终就会导致余额值变成负数,这种情况下就产生了线程安全问题,下面我们就来看看如何解决这个问题。

具体实现

第一步、先来创建一个用户账户表、包括用户ID、账户ID、金额、版本号、以及一个备用标识字段。DDL语句如下。

CREATE TABLE `user_account` (
  `id` int(11) NOT NULL COMMENT '主键',
  `user_id` int(11) DEFAULT NULL COMMENT '账户ID',
  `amount` decimal(10,4) DEFAULT NULL COMMENT '余额',
  `version` int(11) DEFAULT NULL COMMENT '字段编号',
  `spare1` tinyint(11) DEFAULT NULL COMMENT '备用字段',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后再创建一个记录表用来记录提现历史记录。

CREATE TABLE `user_account_record` (
  `id` int(11) NOT NULL COMMENT '主键',
  `account_id` int(11) DEFAULT NULL COMMENT '账户表主键',
  `money` decimal(10,4) DEFAULT NULL COMMENT '体现金额',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

第二步、在Spring Boot项目中创建对应的实体类操作,以及编写Mapper映射文件操作。整体项目结构如下

这里我们重点来关注在UserAccountMapper.xml文件中的两个映射方法即可。代码如下

    <update id="updateAmount">
        update user_account set amount = amount - #{money}
        where spare1 = 1 and id = #{id}
    </update>


    <update id="updateByPKVersion">
        update user_account set amount = amount - #{money},version = version + 1
        where id = #{id} and version = #{version} and amount >0 and  (amount-#{money})>=0
    </update>

这两个方法中一个是用来提供不同的账户金额减扣的逻辑,一个是用来通过乐观锁的方式来进行金额减扣的逻辑。会看到第二种写法中引入了版本version的机制。并且二者的调用逻辑也是有不一样的变化代码如下。

@Service
public class AccountService {
    private static final Logger log = LoggerFactory.getLogger(AccountService.class);
    @Autowired
    private UserAccountMapper userAccountMapper;
    @Autowired
    private UserAccountRecordMapper userAccountRecourdMapper;
    /**
     * 普通体现处理
     * @param userAccountDto
     * @throws Exception
     */
    public void takeMonry(UserAccountDto userAccountDto) throws Exception {
        // 查询用户账户实体记录
        UserAccount userAccount = userAccountMapper.selectUserAccountByUserId(userAccountDto.getUserId());
        // 判断是否有足够的金额来支持体现操作
        if (userAccount!=null && userAccount.getAmount().doubleValue()-userAccountDto.getAmout()>0){
            // 如果可以有足够的金额体现,则更新现有的账户余额
            userAccountMapper.updateAmount(userAccount.getId(),userAccountDto.getAmout());
            // 记录体现成功的记录
            UserAccountRecord record = new UserAccountRecord();
            record.setId((int)(Math.random()*1000));
            record.setCreateTime(new Date());
            record.setAccountId(userAccount.getId());
            record.setMoney(BigDecimal.valueOf(userAccountDto.getAmout()));
            userAccountRecourdMapper.insert(record);
            log.info("提现成功!");
        }else {
            throw new  Exception("账户余额不存在或账户余额不足!");
        }
    }
    
    public void takeMoneyWithLock(UserAccountDto userAccountDto) throws Exception {
        // 查询用户账户实体记录
        UserAccount userAccount = userAccountMapper.selectUserAccountByUserId(userAccountDto.getUserId());
        // 判断是否有足够的金额来支持体现操作
        if (userAccount!=null && userAccount.getAmount().doubleValue()-userAccountDto.getAmout()>0){
            // 如果可以有足够的金额体现,则更新现有的账户余额
            int result = userAccountMapper.updateByPKVersion(userAccountDto.getAmout(), userAccount.getId(), userAccount.getVersion());
            if (result>0){
                // 记录体现成功的记录
                UserAccountRecord record = new UserAccountRecord();
                record.setId((int)(Math.random()*1000));
                record.setCreateTime(new Date());
                record.setAccountId(userAccount.getId());
                record.setMoney(BigDecimal.valueOf(userAccountDto.getAmout()));
                userAccountRecourdMapper.insert(record);
                log.info("提现成功!");
            }else {
                throw new  Exception("账户余额不存在或账户余额不足!");
            }
        }else {
            throw new  Exception("账户余额不存在或账户余额不足!");
        }
    }
}

第三步、验证结果,项目启动之后我们通过JMater压测工具模拟多线程并发访问,会看到在不加锁的情况下用户账户余额包括账户数据都会出现严重的错误。而通过加锁之后的操作,则是没有出现任何的问题。

总结

由于数据库这种加锁的操作在并发量较大的情况下,会在数据库中产生大量的IO操作,会影响到数据库的访问性能,所以说在一些并发特别大的情况下,还是不建议使用这种方式,因为在这种情况下,对于账户数据的操作有可能的是会出现高并发的读写操作。在读写的过程中有可能因为各种原因导致数据不一致等问题出现。

相关推荐

使用 Docker 部署 Java 项目(通俗易懂)

前言:搜索镜像的网站(推荐):DockerDocs1、下载与配置Docker1.1docker下载(这里使用的是Ubuntu,Centos命令可能有不同)以下命令,默认不是root用户操作,...

Spring Boot 3.3.5 + CRaC:从冷启动到秒级响应的架构实践与踩坑实录

去年,我们团队负责的电商订单系统因扩容需求需在10分钟内启动200个Pod实例。当运维组按下扩容按钮时,传统SpringBoot应用的冷启动耗时(平均8.7秒)直接导致流量洪峰期出现30%的请求超时...

《github精选系列》——SpringBoot 全家桶

1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...

Nacos简介—1.Nacos使用简介

大纲1.Nacos的在服务注册中心+配置中心中的应用2.Nacos2.x最新版本下载与目录结构3.Nacos2.x的数据库存储与日志存储4.Nacos2.x服务端的startup.sh启动脚...

spring-ai ollama小试牛刀

序本文主要展示下spring-aiollama的使用示例pom.xml<dependency><groupId>org.springframework.ai<...

SpringCloud系列——10Spring Cloud Gateway网关

学习目标Gateway是什么?它有什么作用?Gateway中的断言使用Gateway中的过滤器使用Gateway中的路由使用第1章网关1.1网关的概念简单来说,网关就是一个网络连接到另外一个网络的...

Spring Boot 自动装配原理剖析

前言在这瞬息万变的技术领域,比了解技术的使用方法更重要的是了解其原理及应用背景。以往我们使用SpringMVC来构建一个项目需要很多基础操作:添加很多jar,配置web.xml,配置Spr...

疯了!Spring 再官宣惊天大漏洞

Spring官宣高危漏洞大家好,我是栈长。前几天爆出来的Spring漏洞,刚修复完又来?今天愚人节来了,这是和大家开玩笑吗?不是的,我也是猝不及防!这个玩笑也开的太大了!!你之前看到的这个漏洞已...

「架构师必备」基于SpringCloud的SaaS型微服务脚手架

简介基于SpringCloud(Hoxton.SR1)+SpringBoot(2.2.4.RELEASE)的SaaS型微服务脚手架,具备用户管理、资源权限管理、网关统一鉴权、Xss防跨站攻击、...

SpringCloud分布式框架&amp;分布式事务&amp;分布式锁

总结本文承接上一篇SpringCloud分布式框架实践之后,进一步实践分布式事务与分布式锁,其中分布式事务主要是基于Seata的AT模式进行强一致性,基于RocketMQ事务消息进行最终一致性,分布式...

SpringBoot全家桶:23篇博客加23个可运行项目让你对它了如指掌

SpringBoot现在已经成为Java开发领域的一颗璀璨明珠,它本身是包容万象的,可以跟各种技术集成。本项目对目前Web开发中常用的各个技术,通过和SpringBoot的集成,并且对各种技术通...

开发好物推荐12之分布式锁redisson-sb

前言springboot开发现在基本都是分布式环境,分布式环境下分布式锁的使用必不可少,主流分布式锁主要包括数据库锁,redis锁,还有zookepper实现的分布式锁,其中最实用的还是Redis分...

拥抱Kubernetes,再见了Spring Cloud

相信很多开发者在熟悉微服务工作后,才发现:以为用SpringCloud已经成功打造了微服务架构帝国,殊不知引入了k8s后,却和CloudNative的生态发展脱轨。从2013年的...

Zabbix/J监控框架和Spring框架的整合方法

Zabbix/J是一个Java版本的系统监控框架,它可以完美地兼容于Zabbix监控系统,使得开发、运维等技术人员能够对整个业务系统的基础设施、应用软件/中间件和业务逻辑进行全方位的分层监控。Spri...

SpringBoot+JWT+Shiro+Mybatis实现Restful快速开发后端脚手架

作者:lywJee来源:cnblogs.com/lywJ/p/11252064.html一、背景前后端分离已经成为互联网项目开发标准,它会为以后的大型分布式架构打下基础。SpringBoot使编码配置...

取消回复欢迎 发表评论: