一,安装需要用到的库
1,go-redis的地址:
https://github.com/go-redis/redis
2,安装go-redis
liuhongdi@ku:~$ go get -u github.com/go-redis/redis/v8
3,redsync的地址
https://github.com/go-redsync/redsync
4,安装redsync
liuhongdi@ku:~$ go get -u github.com/go-redsync/redsync/v4
5,gorm的地址
6,安装gorm
liuhongdi@ku:~$ go get -u gorm.io/gorm
说明:刘宏缔的go森林是一个专注golang的博客,
地址:https://blog.csdn.net/weixin_43881017
说明:作者:刘宏缔 邮箱: 371125307@qq.com
二,演示项目的相关信息
1, 地址:
https://github.com/liuhongdi/digv23
2,功能说明:演示了使用分布式锁避免高并发下单减库存时多扣库存
3, 项目结构;如图:
三,数据库的sql
1,建表sql
- CREATE TABLE `goods` (
- `goodsId` int NOT NULL AUTO_INCREMENT COMMENT 'id',
- `goodsName` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '商品名称',
- `subject` varchar(200) NOT NULL DEFAULT '' COMMENT '标题',
- `price` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
- `stock` int NOT NULL DEFAULT '0' COMMENT '库存数量',
- PRIMARY KEY (`goodsId`)
- ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表'
2,插入演示数据:
- INSERT INTO `goods` (`goodsId`, `goodsName`, `subject`, `price`, `stock`) VALUES
- (1, '蜂蜜牛奶手工皂', '深入滋养,肌肤细腻嫩滑', '70.00', 3),
- (2, '紫光筷子筒', '紫光智护,干爽防潮更健康', '189.00', 40),
- (3, '野性mini便携式蓝牙音箱', '强悍机能,品味豪迈', '499.00', 100),
- (4, '乐穿梭茶具', '茶具+茶叶精美端午礼盒', '200.00', 40);
四,go代码说明
1,controller/goodsController.go
- package controller
- import (
- "github.com/gin-gonic/gin"
- "github.com/liuhongdi/digv23/global"
- "github.com/liuhongdi/digv23/service"
- )
- type GoodsController struct{}
- func NewGoodsController() GoodsController {
- return GoodsController{}
- }
- //购买一件商品
- func (g *GoodsController) BuyOne(c *gin.Context) {
- result := global.NewResult(c)
- var goodsId int64 = 1
- buyNum :=1
- err := service.BuyOneGoods(goodsId,buyNum);
- if err != nil {
- result.Error(404,"数据查询错误")
- } else {
- result.Success("减库存成功");
- }
- return
- }
- //购买一件商品,by lock
- func (g *GoodsController) LockBuyOne(c *gin.Context) {
- result := global.NewResult(c)
- var goodsId int64 = 1
- buyNum :=1
- err := service.LockBuyOneGoods(goodsId,buyNum);
- if err != nil {
- result.Error(404,"数据查询错误")
- } else {
- result.Success("减库存成功");
- }
- return
- }
2,dao/goods.go
- package dao
- import (
- "errors"
- "fmt"
- "github.com/liuhongdi/digv23/global"
- "github.com/liuhongdi/digv23/model"
- "gorm.io/gorm"
- )
- //decrease stock
- func DecreaseOneGoodsStock(goodsId int64,buyNum int) error {
- //查询商品信息
- goodsOne:=&model.Goods{}
- err := global.DBLink.Where("goodsId=?",goodsId).First(&goodsOne).Error
- //fmt.Println(goodsOne)
- if (err != nil) {
- return err
- }
- //得到库存
- stock := goodsOne.Stock
- fmt.Println("当前库存:",stock)
- //fmt.Println(stock)
- if (stock < buyNum || stock <= 0) {
- return errors.New("库存不足")
- }
- //减库存
- result := global.DBLink.Debug().Table("goods").Where("goodsId = ? ", goodsId,buyNum).Update("stock", gorm.Expr("stock - ?", buyNum))
- if (result.Error != nil) {
- return result.Error
- } else {
- fmt.Println("成功减库存一次")
- return nil
- }
- }
3,service/goods.go
- package service
- import (
- "github.com/liuhongdi/digv23/dao"
- "github.com/liuhongdi/digv23/global"
- "strconv"
- "github.com/go-redsync/redsync/v4"
- "github.com/go-redsync/redsync/v4/redis/goredis/v8"
- )
- //购买一件商品
- func BuyOneGoods(goodsId int64,buyNum int) error {
- return dao.DecreaseOneGoodsStock(goodsId,buyNum);
- }
- //购买一件商品,by lock
- func LockBuyOneGoods(goodsId int64,buyNum int) error {
- pool := goredis.NewPool(global.RedisDb) // or, pool := redigo.NewPool(...)
- // Create an instance of redisync to be used to obtain a mutual exclusion
- // lock.
- rs := redsync.New(pool)
- // Obtain a new mutex by using the same name for all instances wanting the
- // same lock.
- mutexname := "goods_"+strconv.FormatInt(goodsId,10)
- mutex := rs.NewMutex(mutexname)
- // Obtain a lock for our given mutex. After this is successful, no one else
- // can obtain the same lock (the same mutex name) until we unlock it.
- if err := mutex.Lock(); err != nil {
- return err
- }
- // Do your work that requires the lock.
- errdecre := dao.DecreaseOneGoodsStock(goodsId,buyNum);
- //fmt.Println(errdecre)
- // Release the lock so other processes or threads can obtain a lock.
- if ok, err := mutex.Unlock(); !ok || err != nil {
- return err
- }
- if (errdecre!=nil){
- return errdecre
- }
- return nil
- }
4,其他相关代码可访问github
五,测试效果
1,设置id为1的商品库存为3
2,测试不加锁的访问:
liuhongdi@ku:~$ ab -c 100 -n 100 http://127.0.0.1:8080/goods/buyone
查看控制台的输出:
- 当前库存: 3
- 2021/01/21 12:22:17 /data/liuhongdi/digv23/dao/goods.go:29
- [1.681ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 当前库存: 2
- 2021/01/21 12:22:18 /data/liuhongdi/digv23/dao/goods.go:29
- [17.357ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- 当前库存: 2
- 当前库存: 1
- 当前库存: 1
- 当前库存: 1
- 当前库存: 1
- 当前库存: 1
- 当前库存: 1
- 2021/01/21 12:22:18 /data/liuhongdi/digv23/dao/goods.go:29
- [39.838ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- 当前库存: 0
- 当前库存: 0
- 2021/01/21 12:22:18 /data/liuhongdi/digv23/dao/goods.go:29
- [85.284ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- 当前库存: 0
- 当前库存: -1
- 当前库存: -1
- 当前库存: -1
- 当前库存: 0
- 当前库存: -1
- 当前库存: -1
- 2021/01/21 12:22:18 /data/liuhongdi/digv23/dao/goods.go:29
- [95.104ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- 当前库存: -2
- ....
注意因为是并发的访问,数据库同时返回了多个结果:库存是2,
导致后面的多个并发执行减库存,使库存数出现负数
3,把库存数重置为3,
测试加锁的减库存:
liuhongdi@ku:~$ ab -c 100 -n 100 http://127.0.0.1:8080/goods/lockbuyone
执行会比较慢,因为每个访问都需要先获得锁之后再执行sql
- DecreaseOneGoodsStock begin
- 当前库存: 3
- 2021/01/21 12:44:16 /data/liuhongdi/digv23/dao/goods.go:30
- [2.115ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- DecreaseOneGoodsStock begin
- 当前库存: 2
- 2021/01/21 12:44:16 /data/liuhongdi/digv23/dao/goods.go:30
- [18.724ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- DecreaseOneGoodsStock begin
- 当前库存: 1
- 2021/01/21 12:44:16 /data/liuhongdi/digv23/dao/goods.go:30
- [2.782ms] [rows:1] UPDATE `goods` SET `stock`=stock - 1 WHERE goodsId = 1
- 成功减库存一次
- DecreaseOneGoodsStock begin
- 当前库存: 0
- DecreaseOneGoodsStock begin
- 当前库存: 0
- DecreaseOneGoodsStock begin
- 当前库存: 0
- DecreaseOneGoodsStock begin
- .....
注意库存数的返回,
因为获取锁之后才查询,所以没有同时返回多个相同数字以致减库存成负数的情况
4,查看redis中的key,注意因为redis的切换很快,不一定可以看到:
- root@ku:/data/liuhongdi/digv23# /usr/local/soft/redis6/bin/redis-cli
- 127.0.0.1:6379> keys *
- 1) "goods_1"
六,查看库的版本:
- module github.com/liuhongdi/digv23
- go 1.15
- require (
- github.com/gin-gonic/gin v1.6.3
- github.com/go-redis/redis/v8 v8.3.3
- gorm.io/driver/mysql v1.0.1
- gorm.io/gorm v1.20.6
- github.com/go-redsync/redsync/v4 v4.0.3
- )
-