分布式锁:不同实现方式实践测评(上)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 分布式锁:不同实现方式实践测评(上)

Hello读者朋友们,今天打算分享一篇测评实践类的文章,用优雅的代码与真实的数据来讲述在分布式场景下,不同方式实现的分布式锁,分别探究每一种方式的性能情况与最终的优劣分析。

开门见山,我们先看一张表格,是由Jmeter测试生成的数据结果,现在不必看懂,本篇文章会从业务场景说起,到分布式锁的引入与分析,相信读完全文后大家就可以融会贯通。

网络异常,图片无法展示
|


1 总体描述与业务实现

说到分布式锁,我们就会想到分布式架构设计的场景,想到分布式架构设计,一般就认定他是为了高并发高负载的业务而起,想到高并发高负载的业务,我们首先可以想到的就是电商平台的秒杀商品场景,没错,通过逆向推导,我们本次的主要业务就是一个电商平台秒杀商品的场景,在此场景下,我们使用分布式锁用来防止商品超卖这个问题,当然此场景下还有诸多需要解决的问题,但是其余我们在此不会涉及。

1.1 场景描述

本次场景就是一个超级简易版的电商秒杀后端系统,大量用户同时购买库存有限的商品,主要流程就是:

用户进行商品购买,商品系统首先查询库存量,足够时则去到数据库进行库存扣减,而后生成通知订单系统生成订单,最终返回给用户,而库存不够时则直接返回用户提示购买失败,不会经过订单系统,所以正常情况下库存量的数量和生成的最大订单量需一致。

因此,我们重点关注的问题就是如何在超大请求量的情况下防止出现超卖现象

库存足够:

网络异常,图片无法展示
|


库存不足:

网络异常,图片无法展示
|


1.2 业务代码实现

安装上面的场景描述,我们使用Go语言+MySQL数据库进行简单的实现:

数据访问层代码:

package dao
import (
   "database/sql"
   "fmt"
   _ "github.com/go-sql-driver/mysql"
)
type Book struct {
   Id    int64  `json:"id"`
   Name  string `json:"name"`
   Price int32  `json:"price"`
   Store int64  `json:"store"`
}
type BookDao struct {
   db     *sql.DB
   tbName string
}
func NewBookDao() *BookDao {
   mysqlDB, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/test?charset=utf8")
   if err != nil {
      fmt.Errorf("Open mysql connection is err  %s", err)
   }
   return &BookDao{db: mysqlDB, tbName: "book"}
}
func (dao *BookDao) GetBookById(id int64) (Book, error) {
   var book Book
   querySql := fmt.Sprintf("SELECT id,name,price,store FROM %s WHERE id=%d", dao.tbName, id)
   rows, err := dao.db.Query(querySql)
   if err != nil {
      fmt.Errorf("GetBookById Query err %s", err)
      return Book{}, err
   }
   if rows.Next() {
      err = rows.Scan(&book.Id, &book.Name, &book.Price, &book.Store)
      if err != nil {
         fmt.Errorf("GetBookById Scan err %s", err)
         return Book{}, err
      }
   }
   return book, nil
}
func (dao *BookDao) UpdateBookStoreById(book Book) error {
   updateSql := fmt.Sprintf("UPDATE %s SET name=?,price=?,store=? WHERE id=?", dao.tbName)
   stmt, err := dao.db.Prepare(updateSql)
   if err != nil {
      fmt.Errorf("UpdateBookStoreById Prepare err %s", err)
      return err
   }
   _, err = stmt.Exec(book.Name, book.Price, book.Store, book.Id)
   return err
}
复制代码

业务层代码:

package service
import (
   "fmt"
   "lock_demo/dao"
)
var orderCount int32 //记录订单数量
func BuyBook(userName string, buyNum int64) string {
   bookDao := dao.NewBookDao()
   book, err := bookDao.GetBookById(1)
   if err != nil {
      fmt.Errorf("GetBookById err %s", err)
      return userName + "购买失败"
   }
   if book.Store >= buyNum && book.Store > 0 {
      book.Store = book.Store - buyNum
      err := bookDao.UpdateBookStoreById(book)
      if err != nil {
         fmt.Errorf("UpdateBookStoreById err %s", err)
         return userName + "购买失败"
      }
      orderCount += 1
      fmt.Printf("===%s 购买成功!数量:%d,剩余:%d,订单量:%d ===\n", userName, buyNum, book.Store, orderCount)
      return userName + "购买成功"
   }
   fmt.Printf("===%s 购买失败!数量:%d,剩余:%d,数量不足===\n", userName, buyNum, book.Store)
   return userName + "购买失败"
}
复制代码

接口层代码:

package main
import (
   "fmt"
   "github.com/spf13/cast"
   "lock_demo/service"
   "net/http"
)
func RunServer() {
   http.HandleFunc("/buyBook", func(w http.ResponseWriter, r *http.Request) {
      username := r.URL.Query().Get("name")
      buyNum := r.URL.Query().Get("num")
      resp := service.BuyBook(username, cast.ToInt64(buyNum))
      _, err := w.Write([]byte(resp))
      if err != nil {
         fmt.Errorf("write err %s", err)
      }
   })
   err := http.ListenAndServe(":8081", nil)
   if err != nil {
      fmt.Errorf("Http run err %s ", err)
   }
}
func main() {
   RunServer()
}
复制代码

项目需要添加的依赖(包括将要使用的分布式锁的依赖):

github.com/go-redis/redis/v8 v8.11.4
   github.com/go-redsync/redsync/v4 v4.6.0
   github.com/go-sql-driver/mysql v1.6.0
   github.com/go-zookeeper/zk v1.0.3
   github.com/spf13/cast v1.5.0
   go.etcd.io/etcd/client/v3 v3.5.5
复制代码

数据库:

CREATE TABLE `book`(
   `id` int(11) NOT NULL,
   `name` varchar(255) NOT NULL,
   `price` double NOT NULL,
   `store` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into book value(1,'Go学习笔记',299,100);
复制代码

1.3 问题的产生

为了更直观的看到问题所在,我们先不加锁,启动项目,访问地址:

GET http://localhost:8081/buyBook
复制代码

加上必要的请求参数

GET http://localhost:8081/buyBook?name=BarryYan&num=1
复制代码

下面我们打开Jmeter工具,新建测试计划,在测试计划中新建线程组和Http请求,我们的参数为20线程循环100次,由上面的SQL语句我们知道,商品的库存只有1000,但是20*100次的请求会有2000的请求量,去抢购1000个商品,正常来讲一定是有1000个用户因为库存不足买不到商品:

网络异常,图片无法展示
|


接下来我们启动Jmeter,查看运行项目的控制台,如下图:

网络异常,图片无法展示
|

等Jmeter执行完成后我们看一下MySQL中还有没有库存:

网络异常,图片无法展示
|


大家可以看到,20*100个请求过后,我们的库存还剩余712,并且订单量也是不正确的,这个问题就是典型的高并发下线程不安全导致的超卖问题。

1.4 分析问题的原因

简述问题的原因,就是在多线程情况下资源被每个线程都获取一份相同的实例,而后在更新库存时每个线程都更新自身操作后的数据,进而每个线程执行过后产生了相同的数据结果,导致了操作多次后数量未发生改变,举个例子:

(1)当前库存900

(2)用户A在D1时间访问数据库看到库存900,用户B在D1时间访问数据库看到库存900

(3)用户A购买1个,库存剩余900-1=899,修改数据库库存为899,用户B也购买1个,库存剩余900-1=899,修改数据库库存为899

(4)此时,数据库剩余库存为899

但是用户A和用户B都获得了订单,库存却只减了1,那么如果D1时间点还有用户C、D、E同时购买呢?结果我们不得而知。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
9天前
|
运维 Kubernetes 调度
阿里云容器服务 ACK One 分布式云容器企业落地实践
阿里云容器服务ACK提供强大的产品能力,支持弹性、调度、可观测、成本治理和安全合规。针对拥有IDC或三方资源的企业,ACK One分布式云容器平台能够有效解决资源管理、多云多集群管理及边缘计算等挑战,实现云上云下统一管理,提升业务效率与稳定性。
|
16天前
|
机器学习/深度学习 存储 运维
分布式机器学习系统:设计原理、优化策略与实践经验
本文详细探讨了分布式机器学习系统的发展现状与挑战,重点分析了数据并行、模型并行等核心训练范式,以及参数服务器、优化器等关键组件的设计与实现。文章还深入讨论了混合精度训练、梯度累积、ZeRO优化器等高级特性,旨在提供一套全面的技术解决方案,以应对超大规模模型训练中的计算、存储及通信挑战。
48 4
|
20天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
38 8
|
3月前
|
运维 Kubernetes 调度
阿里云容器服务 ACK One 分布式云容器企业落地实践
3年前的云栖大会,我们发布分布式云容器平台ACK One,随着3年的发展,很高兴看到ACK One在混合云,分布式云领域帮助到越来越多的客户,今天给大家汇报下ACK One 3年来的发展演进,以及如何帮助客户解决分布式领域多云多集群管理的挑战。
阿里云容器服务 ACK One 分布式云容器企业落地实践
|
4月前
|
存储 分布式计算 Hadoop
【揭秘Hadoop背后的秘密!】HDFS读写流程大曝光:从理论到实践,带你深入了解Hadoop分布式文件系统!
【8月更文挑战第24天】Hadoop分布式文件系统(HDFS)是Hadoop生态系统的关键组件,专为大规模数据集提供高效率存储及访问。本文深入解析HDFS数据读写流程并附带示例代码。HDFS采用NameNode和DataNode架构,前者负责元数据管理,后者承担数据块存储任务。文章通过Java示例演示了如何利用Hadoop API实现数据的写入与读取,有助于理解HDFS的工作原理及其在大数据处理中的应用价值。
118 1
|
4月前
|
机器学习/深度学习 人工智能 负载均衡
【AI大模型】分布式训练:深入探索与实践优化
在人工智能的浩瀚宇宙中,AI大模型以其惊人的性能和广泛的应用前景,正引领着技术创新的浪潮。然而,随着模型参数的指数级增长,传统的单机训练方式已难以满足需求。分布式训练作为应对这一挑战的关键技术,正逐渐成为AI研发中的标配。
211 5
|
4月前
|
存储 Kubernetes 监控
深入浅出分布式事务:理论与实践
在数字化时代的浪潮中,分布式系统如同星辰大海般浩瀚而深邃。本文将带你航行于这片星辰大海,探索分布式事务的奥秘。我们将从事务的基本概念出发,逐步深入到分布式事务的核心机制,最后通过一个实战案例,让你亲自体验分布式事务的魅力。让我们一起揭开分布式事务的神秘面纱,领略其背后的科学与艺术。
95 1
|
4月前
|
UED 存储 数据管理
深度解析 Uno Platform 离线状态处理技巧:从网络检测到本地存储同步,全方位提升跨平台应用在无网环境下的用户体验与数据管理策略
【8月更文挑战第31天】处理离线状态下的用户体验是现代应用开发的关键。本文通过在线笔记应用案例,介绍如何使用 Uno Platform 优雅地应对离线状态。首先,利用 `NetworkInformation` 类检测网络状态;其次,使用 SQLite 实现离线存储;然后,在网络恢复时同步数据;最后,通过 UI 反馈提升用户体验。
106 0
|
4月前
|
机器学习/深度学习 TensorFlow 数据处理
分布式训练在TensorFlow中的全面应用指南:掌握多机多卡配置与实践技巧,让大规模数据集训练变得轻而易举,大幅提升模型训练效率与性能
【8月更文挑战第31天】本文详细介绍了如何在Tensorflow中实现多机多卡的分布式训练,涵盖环境配置、模型定义、数据处理及训练执行等关键环节。通过具体示例代码,展示了使用`MultiWorkerMirroredStrategy`进行分布式训练的过程,帮助读者更好地应对大规模数据集与复杂模型带来的挑战,提升训练效率。
104 0
|
4月前
|
消息中间件 存储 Kafka
微服务实践之分布式定时任务
微服务实践之分布式定时任务