GO微服务中分别用乐观锁,悲观锁,redis分布式锁解决购物平台并发扣库存问题

引言

在购物平台中,扣库存是一个非常关键的操作,涉及到多个并发的操作,比如多个用户同时购买同一商品,或者同时购买多个商品等。在这种情况下,如果不加锁,可能会导致数据不一致、库存错误等问题。因此,当出现以下几种问题时,为了确保数据的一致性和准确性,需要加锁来处理扣库存操作。

  1. 并发访问问题: 在购物平台中,可能有多个用户同时尝试购买同一个商品,或者一个用户同时购买多个商品。如果没有加锁,这些并发的购买操作可能会同时减少库存,导致库存出现错误。

  2. 数据一致性: 扣库存操作是一个涉及多个步骤的复杂操作,可能包括检查库存是否足够、减少库存、生成订单等。如果这些步骤没有在一个原子性的操作中完成,可能会导致数据不一致的情况,例如库存被重复减少或订单生成错误。

  3. 避免超卖: 如果多个用户同时购买同一个商品,并且没有加锁来控制库存的扣减,可能会导致超卖现象,即实际库存不足的情况下仍然允许购买。

  4. 竞态条件: 在并发环境中,竞态条件可能会导致操作顺序的不确定性,从而导致错误的结果。通过加锁,可以防止不同的操作交叉执行,保证操作的有序性。

因此,加锁是为了确保购物平台在多个并发操作中保持数据的一致性、准确性和完整性。下面介绍以下如何在项目中使用悲观锁,乐观锁,redis分布式锁解决并发问题。

悲观锁

  悲观锁是一种悲观的并发控制策略,它认为并发冲突很可能发生。在使用悲观锁时,事务在读取数据时会立即加锁,阻止其他事务对数据进行修改,直到该事务完成。悲观锁通常使用排他锁或共享锁来实现。隔离级别中的可串行化就是使用悲观锁的策略。例如你写的程序需要对商品库存进行自减操作,在分布式系统中,多个用户下单购买商品时,并发访问数据库扣库存,那么在一个扣库存的事务中就可以使用悲观锁,并且悲观锁一般需要和事务配合使用,若不使用事务,锁会在加锁那条查询语句执行完毕后自动释放,而查询之后扣库存的操作就没有持有锁。下面用gorm实现一下悲观锁

db.Transaction(func(tx *gorm.DB) error {
		err := tx.WithContext(ctx).
            Clauses(clause.Locking{Strength:"UPDATE"}).
			Model(&model.Stock{}).
			Where("goods_id = ?", goodsId).
			First(&data).Error
		if err != nil {
			return err
		}
		// 2. 校验;现有库存数>0 且 大于等于num
		if data.Num-num < 0 {
			return errno.ErrUnderstock
		}
		// 3. 扣减
		data.Num -= num
		// 保存
		err = tx.WithContext(ctx).
			Save(&data).Error // save更新所有字段!  97 -> 99 要保证更新的数据是准确的。
		if err != nil {
			zap.L().Error(
				"reduceStock save failed",
				zap.Int64("goods_id", goodsId),
			)
			return err
		}
		return nil
	})

乐观锁

乐观锁是一种乐观的并发控制策略,它认为并发冲突不太常见。在使用乐观锁时,事务在读取数据时不会立即加锁,而是在更新数据时检查是否有其他事务对数据进行了修改。如果发现冲突,事务会回滚或者重新尝试。乐观锁通常通过使用版本号或时间戳等方式来实现。MVCC可以和乐观锁结合使用:在MVCC中,多版本记录的机制提供了一致性的数据快照,而乐观锁可以用来处理在写入时可能发生的并发冲突,当检测到冲突时,事务可以回滚或者重新尝试。 例如你写的程序需要对商品库存进行自减操作,在分布式系统中,多个用户下单购买商品时,并发访问数据库扣库存,那么在一个扣库存的事务中就可以使用乐观锁,在这里我们会让数据表中有个version字段,表示乐观锁版本号,查询数据时获取版本号,然后在更新数据时需要将查询时获得的版本号与当前数据库的版本号进行对比,如果一样就说明没有其他事务修改这条数据,就可以进行更新操作,下面用gorm实现一下乐观锁

func ReduceStock(ctx context.Context, goodsId, num int64) (*model.Stock, error) {
	
        var (
            date model.Stock
            retry = 0
            isSuccess false
        )

	
	   for !isSuccess && retry < 20 {
		err := db.WithContext(ctx).
			Model(&model.Stock{}).
			Where("goods_id = ?", goodsId).
			First(&data).Error
		if err != nil {
			return nil, err
		}
		// 2. 校验;现有库存数>0 且 大于等于num
		if data.Num-num < 0 {
			return errno.ErrUnderstock
		}
		// 3. 扣减
		data.Num -= num
		
		n := db.WithContext(ctx).
			Model(&model.Stock{}).
            Where("goods_id = ? and version = ?",date.GoodsId,date.Version).
            Updates(map[string] interface{}{
            "goods_id": date.GoodsId,
            "num":      date.Num,
            "version":  date.Version + 1,
        }).RowsAffected //updates操作需要判断数据库返回的受影响的行数来判断,因为给nil数据update也会成功,不会返回错误
		if n < 1{
			    fmt.Printf("update err:%v\n", err)
                retry++ //更新失败就重试
                continue
			}
           // 成功更新数据
		isSuccess = true
        break 
     }
     if !isSuccess {
    return nil, errno.ErrReducestockFailer
    }      
       
	return &data, nil
}

分布式锁

Redis可以用于实现分布式锁,其基本原理是利用Redis的单线程特性以及原子性操作来实现在分布式系统中的互斥访问。

实现分布式锁的常见方式之一是使用Redlock算法,该算法基于Redis的分布式特性,但需要在多个Redis实例之间达成共识,因此并不是绝对可靠。以下是基于这种思想的简要分布式锁实现方式:

  1. 获取锁: 当一个进程想要获得锁时,它尝试在Redis中设置一个特定的key-value对。这个key在整个分布式系统中是唯一的,充当锁的标识。

    • 如果这个key之前不存在,那么它被设置成功,该进程获得了锁。
    • 如果这个key之前已经存在(即已经有其他进程持有了锁),那么获取锁的请求可能会失败。这里有几种处理方式:
      • 阻塞等待:进程可以选择在这个key上进行阻塞,直到锁被释放。这可能导致进程被长时间阻塞。
      • 轮询重试:进程可以定期尝试获取锁,避免长时间的阻塞。但是这种方式可能会增加Redis的负载。
  2. 释放锁: 当进程完成了它需要加锁的操作后,它会通过删除对应的key来释放锁,让其他进程有机会获得锁。

func ReduceStock(ctx context.Context, goodsId, num int64) (*model.Stock, error) {
	// 1. 查询现有库存
	var data model.Stock
	// 创建key
	mutexname := fmt.Sprintf("xx-stock-%d", goodsId)
	// 创建锁
	mutex := redis.Rs.NewMutex(mutexname)
	// 获取锁
	if err := mutex.Lock(); err != nil {
		return nil, errno.ErrReducestockFailed
	}

	defer mutex.Unlock() // 释放锁
	// 获取锁成功
	// 开启事务
	db.Transaction(func(tx *gorm.DB) error {
		err := tx.WithContext(ctx).
			Model(&model.Stock{}).
			Where("goods_id = ?", goodsId).
			First(&data).Error
		if err != nil {
			return err
		}
		// 2. 校验;现有库存数>0 且 大于等于num
		if data.Num-num < 0 {
			return errno.ErrUnderstock
		}
		// 3. 扣减
		data.Num -= num
		// 保存
		err = tx.WithContext(ctx).
			Save(&data).Error 
		if err != nil {
			zap.L().Error(
				"reduceStock save failed",
				zap.Int64("goods_id", goodsId),
			)
			return err
		}
		return nil
	})
	return &data, nil
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
乐观锁悲观锁Redis锁都可以用来实现秒杀系统的并发控制。下面是它们的简要介绍: 1. 乐观锁乐观锁是一种乐观地认为并发操作不会发生冲突的锁机制。在秒杀系统,可以使用版本号或时间戳作为乐观锁的标识。当用户发起秒杀请求时,先获取当前商品的版本号或时间戳,然后进行比较。如果版本号或时间戳没有被其他请求修改过,说明没有冲突,可以继续执行秒杀操作,同时将版本号或时间戳更新为新值。如果版本号或时间戳发生变化,说明有其他请求修改了商品信息,此时需要回滚操作或进行重试。 2. 悲观锁悲观锁是一种悲观地认为并发操作会发生冲突的锁机制。在秒杀系统,可以使用数据库的行级锁来实现悲观锁。当用户发起秒杀请求时,先锁定对应商品的行记录,确保其他请求无法修改该记录,然后执行秒杀操作,最后释放锁。悲观锁的缺点是并发度低,性能较差,但可以保证数据的一致性。 3. Redis锁:Redis是一种内存数据库,提供了分布式锁的实现方式。在秒杀系统,可以使用Redis的SETNX命令来实现简单的分布式锁。当用户发起秒杀请求时,先尝试获取Redis指定商品的锁,如果获取成功,则执行秒杀操作,否则等待一段时间后重试。执行完秒杀操作后,释放锁。Redis锁的优点是并发度高,性能较好,但需要注意锁的过期时间设置和处理死锁等异常情况。 以上是三种常见的实现方式,根据具体需求和系统架构选择合适的锁机制来实现秒杀系统的并发控制。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

idMiFeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值