简易秒杀系统-Go语言实现

  • 一、最原始网页
  • 1. 开发环境
  • 2. 部署环境
  • 3. 创建数据库/创建项目工程
  • 4. 搭建初始商品购买网页
  • 二、商品信息静态数据优化
  • 三、(单机)秒杀系统
  • 0. 遇到的问题
  • 1. case1:不加锁,出现超卖现象
  • 2. case2:使用sync包中的Mutex类型的互斥锁,秒杀正常
  • 3. case3:Gin框架的钩子函数/中间件加锁,不能
  • 4. case4:数据库悲观锁(查询加锁),不能
  • 5. case5:数据库悲观锁(更新加锁),正常
  • 6. case6:数据库乐观锁,正常
  • 7. case7:GoLang中的channel,正常
  • 四、分布式秒杀系统


项目地址:https://github.com/Nobodiesljh/seckill-golang

几天速成golang,正好把之前写过的Java版的简易秒杀系统用golang重写一遍,练练手。说是秒杀系统,准确来说是对存在的超卖问题,应用各种锁机制来尝试处理,正好学习一下各种锁的使用。

毕竟还没在生产环境中写过golang,如果有写的不规范或有bug的地方,还望指出,共同进步!

Java版简易秒杀系统GitHub地址:初探并发编程:秒杀系统

个人网站:轨迹

博客:CSDN

一、最原始网页

1. 开发环境

  • GoLand
  • Golang1.16.3
  • Go Module
  • Gin
  • sqlx
  • MySQL5.6+
  • Redis 5.0.3

2. 部署环境

1)除MySQL、Redis外,所有的网页、中间件等都部署在本地win10电脑,配置如下

秒杀一般用在什么java 秒杀软件用什么语言_数据库

2)MySQL5.7 和 Redis 5.0.3 部署在了本地的centos7虚拟机上

3. 创建数据库/创建项目工程

  • 导入数据库脚本,建立好数据库
  • 数据库脚本见数据库脚本文件夹中
  • 创建工程,配置好环境

4. 搭建初始商品购买网页

原本静态的商品购买页面,用从数据库中读取各个商品的数据进行填充,实现不同商品信息的动态查询展示。此时的项目结构如下图

秒杀一般用在什么java 秒杀软件用什么语言_golang_02

访问网址例如:http://localhost:8080/good?gid=739

  • gid=商品编号

二、商品信息静态数据优化

在访问商品信息页面的时候,存在很多静态的、不怎么改动的静态资源信息需要从数据库中读取出来,这部分可以利用缓存等技术进行优化,减少对数据库的访问,提高访问性能

静态数据的缓存可以选择Redis来实现。

对于访问静态页面数据的优化方法还有很多,比如页面的静态化处理、动静分离等,这里就不赘述了

三、(单机)秒杀系统

0. 遇到的问题

1)Error 1040: Too many connections

这是把秒杀用户设置成比较大的数的时候,mysql报的一个错误。这错误好像是当数据库的连接数量陡增的时候就会报出来的一个错误。之前用java实现的秒杀系统中都还没有遇到过这个错误。因为我这里都是创建多个线程来模拟多用户秒杀,所以可见go语言创建线程的轻量级以及速度快。

1. case1:不加锁,出现超卖现象

接口:/seckill/handle?gid=1197

接口内模拟了skillNum个用户进行秒杀,观察控制台日志输出的信息。

可以从t_success_killed表中秒杀订单数量 与 t_promotion_seckill表中商品的剩余数量可以看出,出现了超卖

2. case2:使用sync包中的Mutex类型的互斥锁,秒杀正常

接口:/seckill/handleWithLock?gid=1197

这里注意要用锁把整个事务都包裹起来

3. case3:Gin框架的钩子函数/中间件加锁,不能

Gin框架中存在一个类似于Spring AOP切面编程的一个内容:中间件

但是Gin框架中的中间件,按照我目前的理解,只是相当于一个拦截器,还没有Spring中的切面编程的给函数增强的功能,所以这里不能用它实现加锁了

4. case4:数据库悲观锁(查询加锁),不能

接口:/seckill/handleWithPccOne?gid=1197

这里对查询语句添加for update关键字来加排他锁,实现数据库悲观锁

select ps_count from t_promotion_seckill where goods_id = #{goodsId} for update;
 
当一个请求A开启事务并执行此sql同时未提交事务时,另一个线程B发起请求,此时B将阻塞在加了锁的查询语句上,直到A请求的事务提交或者回滚,B才会继续执行,保证了访问的隔离性。

用Spring实现时就如上面所述,是可以实现数据库的悲观锁的。但是用sqlx框架重写时,也不知道是什么原因并没有锁住数据库。希望得到帮助。下面那个查询加锁的时候倒是正常。存在的一个区别是,case4使用的sqlx下的GET函数执行sql语句,而case5使用的Exec函数执行sql。可能两个函数也有一点区别

5. case5:数据库悲观锁(更新加锁),正常

接口:/seckill/handleWithPccTwo?gid=1197

这里应用mysql数据库本身的特性,即对于UPDATE、DELETE、INSERT语句,InnoDB会自动将涉及的数据集添加排他锁(X锁)。将update商品数量的sql语句上移,并用update操作后返回的结果来判断是否更新成功,来实现数据库悲观锁

6. case6:数据库乐观锁,正常

接口:/seckill/handleWithOcc?gid=1197

利用数据库乐观锁,给秒杀商品信息添加了一个version版本号的字段,来进行版本号的更新。一些用户会因为乐观锁的原因,而被告知下单失败,需要用户再重复操作。

如果秒杀用户比较少的时候,可能会出现少买现象,就是还会剩余一些商品没有被成功卖出去。

7. case7:GoLang中的channel,正常

接口:/seckill/handleWithChannel?gid=1197

BlockingQueue阻塞队列会被频繁的创建和消费,所以需要将其设置成全局使用,并保证一个类只有一个实例,哪怕是多线程同时访问,还需要提供一个全局访问此实例的点。因此,这里使用到了**单例模式**实现

这里SecKillQueue的单例模式是使用类的静态内部类的写法实现的,既保证了线程安全也保证了懒加载,同时也没有加锁的耗费性能的情况。主要是依靠JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载

生产:利用JDK自带的线程安全的阻塞队列LinkedBlockingQueue实现,将秒杀信息添加到阻塞队列中,等待被消费

消费:编写一个实现了ApplicationRunner的启动加载类:BlockingQueueConsumer。项目启动完成后,当有秒杀信息传入阻塞队列时,取出信息消费,进行秒杀处理。这里使用串行消费,所以直接调用没有加锁的方法就可以

上述是Java利用阻塞队列实现时的做法,这里利用Go语言内置的channel的数据结构来复现这个操作。首先,也是利用单例模式创建一个全局唯一的channel,然后将秒杀用户的信息加入到channel中。再启动一个goroutine来消费channel中的数据。

四、分布式秒杀系统

由于时间关系,分布式部分就不重写。各个分布式锁实现的原理也如之前所述。