本文代码地址
本文以扣减库存为例,分别实现进程锁;mysql的悲观锁;乐观锁以及redis的分布式锁
CREATE TABLE `stocks` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`goods` varchar(20) DEFAULT NULL COMMENT '商品id',
`stocks` int(11) DEFAULT NULL COMMENT '库存',
`version` int(11) DEFAULT NULL COMMENT '乐观锁',
PRIMARY KEY (`id`),
KEY `idx_stocks_goods` (`goods`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
package service
import (
context "context"
"go-locks/no-lock/db"
"go-locks/no-lock/model"
"go-locks/no-lock/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type Server struct{}
func (s Server) SellStock(ctx context.Context, request *proto.StockRequest) (*emptypb.Empty, error) {
tx := db.DB.Begin()
for _, info := range request.StockInfos {
var stock model.Stock
if result := tx.Where(&model.Stock{Goods: info.GoodsId}).First(&stock); result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "商品信息不存在")
}
if stock.Stocks < info.Num {
// 库存不足 回滚事务
tx.Rollback()
return nil, status.Error(codes.ResourceExhausted, "库存不足")
}
stock.Stocks -= info.Num
tx.Save(stock)
}
tx.Commit()
return &emptypb.Empty{}, nil
}
package main
import (
"context"
"go-locks/no-lock/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
)
var client proto.StockClient
func main() {
conn, err := grpc.Dial("127.0.0.1:8088", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
client = proto.NewStockClient(conn)
var wg sync.WaitGroup
wg.Add(20)
for i := 0; i < 20; i++ {
go TestSellStock(&wg)
}
wg.Wait()
}
func TestSellStock(wg *sync.WaitGroup) {
defer wg.Done()
_, err := client.SellStock(context.Background(), &proto.StockRequest{
StockInfos: []*proto.StockInfo{
{
GoodsId: "123456",
Num: 1,
},
},
})
if err != nil {
panic(err)
}
}
开启20gorouinte去扣减123456的库存;正常情况下123456的库存应该剩余480件,但由于我们没有进行加锁,导致库存还剩485件.这种情况在真实场景下是绝对不能接受的
package service
import (
context "context"
"go-locks/process-lock/db"
"go-locks/process-lock/model"
"go-locks/process-lock/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"sync"
)
type Server struct{}
var mutex sync.Mutex
func (s Server) SellStock(ctx context.Context, request *proto.StockRequest) (*emptypb.Empty, error) {
// 加锁
mutex.Lock()
tx := db.DB.Begin()
for _, info := range request.StockInfos {
var stock model.Stock
if result := tx.Where(&model.Stock{Goods: info.GoodsId}).First(&stock); result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "商品信息不存在")
}
if stock.Stocks < info.Num {
// 库存不足 回滚事务
tx.Rollback()
return nil, status.Error(codes.ResourceExhausted, "库存不足")
}
stock.Stocks -= info.Num
tx.Save(stock)
}
tx.Commit()
// 解锁
mutex.Unlock()
return &emptypb.Empty{}, nil
}
package main
import (
"context"
"go-locks/process-lock/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
)
var client proto.StockClient
func main() {
conn, err := grpc.Dial("127.0.0.1:8088", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
client = proto.NewStockClient(conn)
var wg sync.WaitGroup
wg.Add(20)
for i := 0; i < 20; i++ {
go TestSellStock(&wg)
}
wg.Wait()
}
func TestSellStock(wg *sync.WaitGroup) {
defer wg.Done()
_, err := client.SellStock(context.Background(), &proto.StockRequest{
StockInfos: []*proto.StockInfo{
{
GoodsId: "123457",
Num: 1,
},
},
})
if err != nil {
panic(err)
}
}
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
package service
import (
context "context"
"go-locks/pessimistic-lock/db"
"go-locks/pessimistic-lock/model"
"go-locks/pessimistic-lock/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"gorm.io/gorm/clause"
)
type Server struct{}
func (s Server) SellStock(ctx context.Context, request *proto.StockRequest) (*emptypb.Empty, error) {
tx := db.DB.Begin()
for _, info := range request.StockInfos {
var stock model.Stock
// 通过for update 语句实现mysql的悲观锁
if result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(&model.Stock{Goods: info.GoodsId}).First(&stock); result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "商品信息不存在")
}
if stock.Stocks < info.Num {
// 库存不足 回滚事务
tx.Rollback()
return nil, status.Error(codes.ResourceExhausted, "库存不足")
}
stock.Stocks -= info.Num
tx.Save(stock)
}
tx.Commit()
return &emptypb.Empty{}, nil
}
package main
import (
"context"
"go-locks/pessimistic-lock/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
)
var client proto.StockClient
func main() {
conn, err := grpc.Dial("127.0.0.1:8088", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
client = proto.NewStockClient(conn)
var wg sync.WaitGroup
wg.Add(20)
for i := 0; i < 20; i++ {
go TestSellStock(&wg)
}
wg.Wait()
}
func TestSellStock(wg *sync.WaitGroup) {
defer wg.Done()
_, err := client.SellStock(context.Background(), &proto.StockRequest{
StockInfos: []*proto.StockInfo{
{
GoodsId: "123458",
Num: 1,
},
},
})
if err != nil {
panic(err)
}
}
乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,
package service
import (
context "context"
"go-locks/optimistic-lock/db"
"go-locks/optimistic-lock/model"
"go-locks/optimistic-lock/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"log"
)
type Server struct{}
func (s Server) SellStock(ctx context.Context, request *proto.StockRequest) (*emptypb.Empty, error) {
tx := db.DB.Begin()
for _, info := range request.StockInfos {
var stock model.Stock
for {
if result := db.DB.Where(&model.Stock{Goods: info.GoodsId}).First(&stock); result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "商品信息不存在")
}
if stock.Stocks < info.Num {
// 库存不足 回滚事务
tx.Rollback()
return nil, status.Error(codes.ResourceExhausted, "库存不足")
}
stock.Stocks -= info.Num
if result := tx.Model(&model.Stock{}).Select("Stocks", "Version").
Where("goods = ? AND version = ?", info.GoodsId, stock.Version).Updates(&model.Stock{Stocks: stock.Stocks, Version: stock.Version + 1}); result.RowsAffected == 0 {
// version 字段冲突;扣减失败
log.Println("库存扣减失败;重试")
} else {
// 库存扣减成功
log.Println("库存扣减成功")
break
}
}
}
tx.Commit()
return &emptypb.Empty{}, nil
}
package main
import (
"context"
"go-locks/optimistic-lock/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
)
var client proto.StockClient
func main() {
conn, err := grpc.Dial("127.0.0.1:8088", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
client = proto.NewStockClient(conn)
var wg sync.WaitGroup
wg.Add(20)
for i := 0; i < 20; i++ {
go TestSellStock(&wg)
}
wg.Wait()
}
func TestSellStock(wg *sync.WaitGroup) {
defer wg.Done()
_, err := client.SellStock(context.Background(), &proto.StockRequest{
StockInfos: []*proto.StockInfo{
{
GoodsId: "123459",
Num: 1,
},
},
})
if err != nil {
panic(err)
}
}
package service
import (
context "context"
"fmt"
"go-locks/redis-lock/db"
"go-locks/redis-lock/model"
"go-locks/redis-lock/proto"
"go-locks/redis-lock/redis"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type Server struct{}
func (s Server) SellStock(ctx context.Context, request *proto.StockRequest) (*emptypb.Empty, error) {
tx := db.DB.Begin()
for _, info := range request.StockInfos {
var stock model.Stock
// 使用redis分布式锁,仅对当前商品进行加锁,不会影响其他商品
mutex := redis.Redsy.NewMutex(fmt.Sprintf("goods_%s", info.GoodsId))
if err := mutex.Lock(); err != nil {
return nil, status.Error(codes.Internal, "获取redis分布式锁异常")
}
if result := tx.Where(&model.Stock{Goods: info.GoodsId}).First(&stock); result.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "商品信息不存在")
}
if stock.Stocks < info.Num {
// 库存不足 回滚事务
tx.Rollback()
return nil, status.Error(codes.ResourceExhausted, "库存不足")
}
stock.Stocks -= info.Num
tx.Save(stock)
if ok, err := mutex.Unlock(); !ok || err != nil {
return nil, status.Error(codes.Internal, "释放redis分布式锁异常")
}
}
tx.Commit()
return &emptypb.Empty{}, nil
}
package main
import (
"context"
"go-locks/redis-lock/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
)
var client proto.StockClient
func main() {
conn, err := grpc.Dial("127.0.0.1:8088", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
client = proto.NewStockClient(conn)
var wg sync.WaitGroup
wg.Add(20)
for i := 0; i < 20; i++ {
go TestSellStock(&wg)
}
wg.Wait()
}
func TestSellStock(wg *sync.WaitGroup) {
defer wg.Done()
_, err := client.SellStock(context.Background(), &proto.StockRequest{
StockInfos: []*proto.StockInfo{
{
GoodsId: "123460",
Num: 1,
},
},
})
if err != nil {
panic(err)
}
}