项目总结--需求:抢购商品

–需求:抢购商品

涉及问题:并发问题,库存变更,活动时间

涉及技术栈:redis,go

1.并发方案

     -使用redis的原子性的特质实现并发问题。也是较为常用的解决方案
     -因为redis是单线程运行,不管同时有多少并发,都只会执行一个语句,所以可以用来解	决并发问题

实际操作:

      1,让商品的唯一标识ID作为下表,再加上项目前缀确保唯一性。value为库存
      2,每次减库存前先判断库存量是否足够然后再执行减库存操作
      3,使用decyby减少数量
      4,如果decyby后数量小于0,返回错误并将库存加回去
      5,同步到MySQL中

问题:

      因为减库存时执行了两个操作(查看+修改)这两个其实已经破坏了原子性操作,所以会有一点并发的问题
      例如:有三个库存,两个用户A,B。A要买2个,B也要买2个,

操作顺序

操作1:A判断数量是否足够,yes
操作2:B判断数量是否足够,yes
操作3:A减少数量 	yes
操作4:B减少数量	no
操作5:B归还库存数量(因为小于1)yes

    这种情况说明假如再一个用户还没有实际减库存之前,就有另外一个用户读到库存了,就会产生这种类似于幻读
     的问题。那为什么仍然要用这种方案呢,不把第一个判断库存数量删除?
     
    因为假如不判断的话他仍然不是原子性操作,因为(修改+归还库存-库存不足条件下)这也会有并发问题
 	例如:有三个库存,两个用户A,B。A要买4个,B也要买2个,

操作序列

	操作1:A减少数量 	no
	操作2:B减少数量	no
	操作3:A归还库存数量(因为小于0)yes
	操作4:B归还库存数量(因为小于0)yes

可以看到即便剩余4个库存,B要买2个,也会购买失败,并且在实际测试中发现这种比例近乎1/4,这是业务上
不能接受的。于是选择了第一种在减少库存前,先判断一下数量。
     但是第一种例子的问题其实和第二种类似,但是他的比例就已经降低了许多,多次尝试都没有出现问题,
     对于业务上来说可以接受,所以最终选择了这个方案。虽然这个概率比实际偏低(测试手段问题)

其他方案

其他方案-1:
如果想要完全解决并发问题,可以使用redis中的list数据结构,通过list的len来代表库存。这样的话在库存为
0时,redis会自己报错。不需要我们再操作,这样就可以保证原子性,但是这样会有一个弊端,redis并不支持
pop指定数量的节点,
所以这种方案只有确定了每个用户限购1个的业务要求下才能使用
其他方案-2
在实际使用的方案上确保操作的原子性,可以增加redis的事务。并将三步操作简化为2步即把判断库存是否足够
删掉。因内已经保证了还库存这一操作的原子性,所以已经不存在库存足够,但是却买不到的问题
但是这种方案有局限性。因为redis的事务并不是真正意义上的事务,他不能完全保证原子性。且会影响抢购时
的性能,(这个方案是我提出的)最后被pass掉了

实现代码:
package main


```go
import (
	"context"
	"fmt"
	"math/rand"
	"strconv"
	"sync"
	"time"

	"github.com/go-redis/redis"
)

var ctx = context.Background()
var wg sync.WaitGroup

func main() {
	startTime := time.Now().UnixNano()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       0,
	})
	err := rdb.Set("stock", 10, 0).Err()
	if err != nil {
		fmt.Println("设置库存失败!")
	}
	//设置库存
	oldStock := rdb.Get("stock")
	fmt.Println("当前库存数量为: ", oldStock.Val())
	wg.Add(50)
	for i := 0; i < 50; i++ {
		go func() {
			rand.Seed(time.Now().UnixMicro())
			buyNumber := rand.Intn(3) + 1 //购买数量1-4随机
			//提前判断一下库存,减少符号条件但购买失败的概率
			TmpStock := rdb.Get("stock")
			Tmp, _ := strconv.Atoi(TmpStock.Val())
			if Tmp < buyNumber {
				fmt.Println(strconv.Itoa(buyNumber) + "件购买失败!")
				wg.Done()
				return
			}
			res := rdb.DecrBy("stock", int64(buyNumber))
			if res.Val() < 0 {
				fmt.Println(strconv.Itoa(buyNumber) + "件购买失败!")
				rdb.IncrBy("stock", int64(buyNumber))
			} else {
				fmt.Println(strconv.Itoa(buyNumber) + "件购买成功!")
			}
			wg.Done()
		}()
	}
	wg.Wait()
	//设置库存
	newStock := rdb.Get("stock")
	fmt.Println("当前库存数量为: ", newStock)
	fmt.Println("运行时间:(ns)", time.Now().UnixNano()-startTime)
}

# 2, 库存变更

	其实第一部分是redis部分的库存变更,已经把重点讲完了,针对MySQL部分的库存变更,用一般的方法也是+一个事务,同时
     设置一个字段version用来加乐观锁。同时要注意在事务中,讲涉及到+行锁的更新操作后移,减少行锁时间。

# 3,活动时间

	活动时间的不同会导致活动的状态不同,待审核,待发布,待开始,进行中,已结束,取消。
	可以将待开始,进行中, 已结束整合成一个状态。在展示的列表接口或者状态判断时使用time.now进行判断,虽然代码会
	更复杂一点,但是时间判断更加准确。
	但是这种方法有一种情况不能使用,在开始,结束等时间点,需要修改表中字段时,此方法没有办法修改,只做展示用。还是
	需要跑定时脚本
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值