自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

基于 Golang 和 Redis 解決分布式系統(tǒng)下的并發(fā)問題

云計(jì)算 分布式
Redis 提供了多種解決并發(fā)問題的方案,包括原子操作、事務(wù)、LUA 腳本和分布式鎖等。在實(shí)際應(yīng)用中,需要根據(jù)具體場景選擇合適的方案。

在分布式系統(tǒng)和數(shù)據(jù)庫的交互中,并發(fā)問題如同暗流般潛伏,稍有不慎就會(huì)掀起應(yīng)用的驚濤駭浪。試想一下,我們正在構(gòu)建一個(gè)股票交易平臺(tái),允許不同用戶同時(shí)購買公司股票。每個(gè)公司都有一定數(shù)量的可用股票,用戶只能在剩余股票充足的情況下進(jìn)行購買。

Golang 與 Redis 的解決方案:構(gòu)建穩(wěn)固的交易系統(tǒng)

為了解決這個(gè)問題,我們可以借助 Golang 和 Redis 的強(qiáng)大功能,構(gòu)建一個(gè)安全可靠的交易系統(tǒng)。

數(shù)據(jù)層搭建:GoRedis 助力高效交互

首先,我們使用 goredis 客戶端庫創(chuàng)建一個(gè)數(shù)據(jù)層(Repository),用于與 Redis 數(shù)據(jù)庫進(jìn)行交互:

type Repository struct {
 client *redis.Client
}

var _ go_redis_concurrency.Repository = (*Repository)(nil)

func NewRepository(address, password string) Repository {
 return Repository{
  client: redis.NewClient(&redis.Options{
   Addr:     address,
   Password: password,
  }),
 }
}

購買股票功能實(shí)現(xiàn):并發(fā)問題初現(xiàn)端倪

接下來,我們實(shí)現(xiàn) BuyShares 函數(shù),模擬用戶購買股票的操作:

func (r *Repository) BuyShares(ctx context.Context, userId, companyId string, numShares int, wg *sync.WaitGroup) error {
 defer wg.Done()

 companySharesKey := BuildCompanySharesKey(companyId)

 // --- (1) ----
 // 獲取當(dāng)前可用股票數(shù)量
 currentShares, err := r.client.Get(ctx, companySharesKey).Int()
 if err != nil {
  fmt.Print(err.Error())
  return err
 }

 // --- (2) ----
 // 驗(yàn)證剩余股票是否充足
 if currentShares < numShares {
  fmt.Print("error: 公司剩余股票不足\n")
  return errors.New("error: 公司剩余股票不足")
 }
 currentShares -= numShares

 // --- (3) ----
 // 更新公司可用股票數(shù)量
 _, err = r.client.Set(ctx, companySharesKey, currentShares, 0).Result()
 return err
}

該函數(shù)包含三個(gè)步驟:

  1. 獲取公司當(dāng)前可用股票數(shù)量。
  2. 驗(yàn)證剩余股票是否足以滿足用戶購買需求。
  3. 更新公司可用股票數(shù)量。

看似邏輯清晰,但當(dāng)多個(gè)用戶并發(fā)執(zhí)行 BuyShares 函數(shù)時(shí),問題就出現(xiàn)了。

模擬并發(fā)場景:問題暴露無遺

為了模擬并發(fā)場景,我們創(chuàng)建多個(gè) Goroutine 同時(shí)執(zhí)行 BuyShares 函數(shù):

const (
 total_clients = 30
)

func main() {
 // --- (1) ----
 // 初始化 Repository
 repository := redis.NewRepository(fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port), config.Redis.Pass)

 // --- (2) ----
 // 并發(fā)執(zhí)行 BuyShares 函數(shù)
 companyId := "TestCompanySL"
 var wg sync.WaitGroup
 wg.Add(total_clients)

 for idx := 1; idx <= total_clients; idx++ {
  userId := fmt.Sprintf("user%d", idx)
  go repository.BuyShares(context.Background(), userId, companyId, 100, &wg)
 }
 wg.Wait()

 // --- (3) ----
 // 獲取公司剩余股票數(shù)量
 shares, err := repository.GetCompanyShares(context.Background(), companyId)
 if err != nil {
  panic(err)
 }
 fmt.Printf("公司 %s 剩余股票數(shù)量: %d\n", companyId, shares)
}

假設(shè)公司 TestCompanySL 初始擁有 1000 股可用股票,每個(gè)用戶購買 100 股。我們期望的結(jié)果是,只有 10 個(gè)用戶能夠成功購買股票,剩余用戶會(huì)因?yàn)楣善辈蛔愣盏藉e(cuò)誤信息。

然而,實(shí)際運(yùn)行結(jié)果卻出乎意料,公司剩余股票數(shù)量可能出現(xiàn)負(fù)數(shù),這意味著多個(gè)用戶在讀取可用股票數(shù)量時(shí),獲取到的是同一個(gè)未更新的值,導(dǎo)致最終結(jié)果出現(xiàn)偏差。

Redis 并發(fā)解決方案:精準(zhǔn)打擊,逐個(gè)擊破

為了解決上述并發(fā)問題,Redis 提供了多種解決方案,讓我們來一一剖析。

原子操作:簡單場景下的利器

原子操作能夠在不加鎖的情況下,保證對(duì)數(shù)據(jù)的修改操作具有原子性。在 Redis 中,可以使用 INCRBY 命令對(duì)指定 key 的值進(jìn)行原子遞增或遞減。

func (r *Repository) BuyShares(ctx context.Context, userId, companyId string, numShares int, wg *sync.WaitGroup) error {
 defer wg.Done()

 // ... (省略部分代碼) ...

 // 使用 INCRBY 命令原子更新股票數(shù)量
 _, err = r.client.IncrBy(ctx, companySharesKey, int64(-numShares)).Result()
 return err
}

然而,在我們的股票交易場景中,原子操作并不能完全解決問題。因?yàn)樵诟鹿善睌?shù)量之前,還需要進(jìn)行剩余股票數(shù)量的驗(yàn)證。如果多個(gè)用戶同時(shí)讀取到相同的可用股票數(shù)量,即使使用原子操作更新,最終結(jié)果仍然可能出現(xiàn)錯(cuò)誤。

事務(wù):保證操作的原子性

Redis 事務(wù)可以將多個(gè)命令打包成一個(gè)原子操作,要么全部執(zhí)行成功,要么全部回滾。通過 MULTI、EXEC、DISCARD 和 WATCH 命令,可以實(shí)現(xiàn)對(duì)數(shù)據(jù)的原子性操作。

  • MULTI:標(biāo)記事務(wù)塊的開始。
  • EXEC:執(zhí)行事務(wù)塊中的所有命令。
  • DISCARD:取消事務(wù)塊,放棄執(zhí)行所有命令。
  • WATCH:監(jiān)視指定的 key,如果 key 在事務(wù)執(zhí)行之前被修改,則事務(wù)執(zhí)行失敗。

在我們的例子中,可以使用 WATCH 命令監(jiān)視公司可用股票數(shù)量的 key。如果 key 在事務(wù)執(zhí)行之前被修改,則說明有其他用戶并發(fā)修改了數(shù)據(jù),當(dāng)前事務(wù)執(zhí)行失敗,從而保證數(shù)據(jù)的一致性。

func (r *Repository) BuyShares(ctx context.Context, userId, companyId string, numShares int, wg *sync.WaitGroup) error {
 defer wg.Done()

 companySharesKey := BuildCompanySharesKey(companyId)

 // 使用事務(wù)保證操作的原子性
 tx := r.client.TxPipeline()
 tx.Watch(ctx, companySharesKey)

 // ... (省略部分代碼) ...

 _, err = tx.Exec(ctx).Result()
 return err
}

然而,在高并發(fā)場景下,使用事務(wù)可能會(huì)導(dǎo)致大量事務(wù)執(zhí)行失敗,影響系統(tǒng)性能。

LUA 腳本:將邏輯移至 Redis 服務(wù)端執(zhí)行

為了避免上述問題,可以借助 Redis 的 LUA 腳本功能,將業(yè)務(wù)邏輯移至 Redis 服務(wù)端執(zhí)行。LUA 腳本在 Redis 中以原子方式執(zhí)行,可以有效避免并發(fā)問題。

local sharesKey = KEYS[1]
local requestedShares = ARGV[1]

local currentShares = redis.call("GET", sharesKey)
if currentShares < requestedShares then
 return {err = "error: 公司剩余股票不足"}
end

currentShares = currentShares - requestedShares
redis.call("SET", sharesKey, currentShares)

該 LUA 腳本實(shí)現(xiàn)了與 BuyShares 函數(shù)相同的邏輯,包括獲取可用股票數(shù)量、驗(yàn)證剩余股票是否充足以及更新股票數(shù)量。

在 Golang 中,可以使用 goredis 庫執(zhí)行 LUA 腳本:

var BuyShares = redis.NewScript(`
 local sharesKey = KEYS[1]
 local requestedShares = ARGV[1]

 local currentShares = redis.call("GET", sharesKey)
 if currentShares < requestedShares then
  return {err = "error: 公司剩余股票不足"}
 end

 currentShares = currentShares - requestedShares
 redis.call("SET", sharesKey, currentShares)
`)

func (r *Repository) BuyShares(ctx context.Context, userId, companyId string, numShares int, wg *sync.WaitGroup) error {
 defer wg.Done()

 keys := []string{BuildCompanySharesKey(companyId)}
 err := BuyShares.Run(ctx, r.client, keys, numShares).Err()
 if err != nil {
  fmt.Println(err.Error())
 }
 return err
}

使用 LUA 腳本可以有效解決并發(fā)問題,并且性能優(yōu)于事務(wù)機(jī)制。

分布式鎖:靈活控制并發(fā)訪問

除了 LUA 腳本,還可以使用分布式鎖來控制對(duì)共享資源的并發(fā)訪問。Redis 提供了 SETNX 命令,可以實(shí)現(xiàn)簡單的分布式鎖機(jī)制。

在 Golang 中,可以使用 redigo 庫的 Lock 函數(shù)獲取分布式鎖:

func (r *Repository) BuyShares(ctx context.Context, userId, companyId string, numShares int, wg *sync.WaitGroup) error {
 defer wg.Done()

 companySharesKey := BuildCompanySharesKey(companyId)

 // 獲取分布式鎖
 lockKey := "lock:" + companySharesKey
 lock, err := r.client.Lock(ctx, lockKey, redislock.Options{
  RetryStrategy: redislock.ExponentialBackoff{
   InitialDuration: time.Millisecond * 100,
   MaxDuration:     time.Second * 3,
  },
 })
 if err != nil {
  return fmt.Errorf("獲取分布式鎖失敗: %w", err)
 }
 defer lock.Unlock(ctx)

 // ... (省略部分代碼) ...

 return nil
}

使用分布式鎖可以靈活控制并發(fā)訪問,但需要謹(jǐn)慎處理鎖的釋放和超時(shí)問題,避免出現(xiàn)死鎖情況。

總結(jié)

Redis 提供了多種解決并發(fā)問題的方案,包括原子操作、事務(wù)、LUA 腳本和分布式鎖等。在實(shí)際應(yīng)用中,需要根據(jù)具體場景選擇合適的方案。

  • 原子操作適用于簡單場景,例如計(jì)數(shù)器等。
  • 事務(wù)可以保證多個(gè)操作的原子性,但性能較低。
  • LUA 腳本可以將業(yè)務(wù)邏輯移至 Redis 服務(wù)端執(zhí)行,性能較高,但需要熟悉 LUA 語法。
  • 分布式鎖可以靈活控制并發(fā)訪問,但需要謹(jǐn)慎處理鎖的釋放和超時(shí)問題。

希望本文能夠幫助你更好地理解和解決 Redis 并發(fā)問題,構(gòu)建更加穩(wěn)定可靠的分布式系統(tǒng)。

責(zé)任編輯:武曉燕 來源: 源自開發(fā)者
相關(guān)推薦

2021-10-25 09:50:57

Redis分布式技術(shù)

2021-10-26 00:38:10

Redis分布式

2021-06-03 00:02:43

RedisRedlock算法

2021-07-30 00:09:21

Redlock算法Redis

2021-12-01 10:13:48

場景分布式并發(fā)

2022-03-08 15:24:23

BitMapRedis數(shù)據(jù)

2023-05-18 14:02:00

分布式系統(tǒng)冪等性

2019-06-19 15:40:06

分布式鎖RedisJava

2017-12-12 14:51:15

分布式緩存設(shè)計(jì)

2020-02-17 16:05:17

系統(tǒng)演進(jìn)過程時(shí)間問題

2021-12-14 08:19:59

系統(tǒng)分布式網(wǎng)絡(luò)

2021-12-15 07:24:56

分布式系統(tǒng)時(shí)鐘

2020-10-19 07:30:57

Java Redis 開發(fā)

2022-03-08 07:22:48

Redis腳本分布式鎖

2017-06-05 15:51:54

分布式Logical Tim算法

2023-05-12 08:23:03

分布式系統(tǒng)網(wǎng)絡(luò)

2013-12-20 09:43:13

分布式

2020-09-23 09:52:01

分布式WebSocketMQ

2019-12-26 08:59:20

Redis主從架構(gòu)

2023-02-11 00:04:17

分布式系統(tǒng)安全
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)