【原】Redis乐观锁-商品秒杀

/ 0评 / 0

应用场景:我们有一个商品computer,在整点开启秒杀,共有5台,这时候我们可能需要在用户提交订单的时候讲商品余量进行减一,然后再把这个余量放到Redis里。

场景分析:这里的解决方案其实有很多种,如果限制没人秒杀1台,则直接使用incr指令就可以保证操作的原子性;也可以使用分布式锁来避免冲突,这是一个很好的方案。但是这里我做的是一个模拟场景,也许限时抢购口罩的事情大家都经历过,同一id每次最多秒杀3包口罩,这时候我们在使用incr指令好像就不是很合适了,不过仍然可以使用分布式锁。

分布式锁是一种悲观锁,本文着重讲解如何使用乐观锁来模拟解决冲突。

Redis里有一种机制是watch,它就是一种乐观锁,有了watch,我们又多了一种可以解决并发修改的方法,watch的用法如下:

for true{
    do_watch();
    commands();
    multi();
    send_commands();
    try:
    	exec();
    	break;
    except WatchError:
    	continue;
}

watch会在事务开始之前盯住1个或者多个变量,当事务exec时,Redis会检查关键变量自watch之后,是否被修改了(包括当前事务的客户端)。如果被改过了,则会返回null,告知客户端执行失败,这时候客户端通常会重试。使用Redis指令操作如下:

127.0.0.1:6379> watch computer      #监控computer
OK
127.0.0.1:6379> incr computer      #被修改了
(integer) 1
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> incr computer
QUEUED
127.0.0.1:6379> exec                #提示执行失败
(nil)

服务端给exec指令返回null的时候,客户端知道了事务执行失败,有的编程语言会抛出一个异常,有的则会直接返回一个null,这样客户端来判断一下即可知道是否执行失败。

注意事项:Redis禁止在multi和exec之间执行watch指令,必须在multi之前就执行watch盯好关键变量,否则会出错。下面我使用golang来模拟一个使用乐观锁来解决秒杀并发冲突的案例,

package main

import (
	"errors"
	"github.com/go-redis/redis"
	"log"
	"sync"
	"time"
)

func main() {

	rdb := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       0,
	})
	key := "computer"
    
	rdb.Set(key,5,5 * time.Minute)
	val,_ := rdb.Get(key).Result()
	log.Println("当前剩余电脑: ", val)
	routineCount := 8 //假设当前最多进程数
	wg := sync.WaitGroup{}
	wg.Add(routineCount)
    //开启抢购
	for i:= 0;i< routineCount;i++{
		go func(id int) {
			defer wg.Done()
			for  {
				//监控电脑的剩余数量
				err := rdb.Watch(SecKill(key),key)
				if err == nil {
					log.Println(id,"秒杀成功")
					return
				}else if err.Error() == "库存不足" {
					log.Println(id,"库存不足")
					return
				}else {
					log.Println(err,"稍后重试",id)
				}
			}
		}(i)
	}
	wg.Wait()
}
//秒杀方法
func SecKill(key string) func(tx *redis.Tx) error {
	txf := func(tx *redis.Tx) error {
		//获取剩余商品数量
		n,err := tx.Get(key).Int()
		if err != nil && err != redis.Nil {
			return  err
		}
		if n == 0 {
			return  errors.New("库存不足")
		}
		//减库存
		n--
        //开启事务,设置减掉后的新库存,并且只有被监视的关键点保持不变时才运行
		_,err = tx.TxPipelined(func(pip redis.Pipeliner) error {
			//
			pip.Set(key,n,5 * time.Minute)
			return  nil
		})
		return  err
	}
	return txf
}

这里我把key的失效时间设置了5分钟,具体使用的时候要根据需求自己规划。

最近博主在苦学Redis,书里的每个Redis教程都会做实验,也会将自己理解的比较好,做的不错的实验分享出来,有不对的地方希望大家能帮忙改正~~