简单场景:一个下单按钮,调用API, 库存减去1

对于一般的访问量不高的,代码很简单:

直接从sql获取库存,然后减一,然而当并发量提高的时候,从数据库获取,再到减一的过程中,库存已经不是当时的库存了,我们可能想到很多解决办法,表锁,时间戳,代码锁,但是高并发的时候每次都请求数据库是不合理的,所以我们使用Redis。 .net core 下可以引入CSRedis,我们把库存放入到内存中,这样性能提升了。

但是如果按照刚才的那种处理方法,直接容redis中获取,然后减一,还是会有同样的问题,所以我们需要加入锁,在 net core 异步编程中,lock下是不能使用await的,我们可以使用SemaphoreSlim,让每次进来的线程数只能是1

public class TestController : Controller
    {
        static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
        private StackRedis _redis = StackRedis.Current;
        ILogger<TestController> logger;

        public TestController(ILogger<TestController> logger)
        {
            this.logger = logger;
        }
        // GET: api/<controller>
        [HttpGet]
        public async Task<IEnumerable<string>> GetAsync()
        {
                await semaphoreSlim.WaitAsync();
            
                int number = Convert.ToInt32(await _redis.Get("number"));
                if (number > 0)
                {
                    number--;
                }
                else
                {
                    logger.LogInformation("没有库存");
                }
                logger.LogInformation(number + "");
                await _redis.Set("number", number.ToString());
                semaphoreSlim.Release();
                return new string[] { "value1", "value2" };
            

        }
  
    }
}

看到这里,感觉代码已经没什么问题了,其实不对,万一程序执行不到释放线程的那一步,就是之前出现异常了,那么线程岂不是永远不释放,所以需要改下代码

public class TestController : Controller
    {
        static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
        private StackRedis _redis = StackRedis.Current;
        ILogger<TestController> logger;

        public TestController(ILogger<TestController> logger)
        {
            this.logger = logger;
        }
        // GET: api/<controller>
        [HttpGet]
        public async Task<IEnumerable<string>> GetAsync()
        {
            await semaphoreSlim.WaitAsync();
            try
            {
                int number = Convert.ToInt32(await _redis.Get("number"));
                if (number > 0)
                {
                    number--;
                }
                else
                {
                    logger.LogInformation("没有库存");
                }
                logger.LogInformation(number + "");
                await _redis.Set("number", number.ToString());
                return new string[] { "value1", "value2" };
            }
            finally
            {
                
                semaphoreSlim.Release();
            }


        }

       
    }
}

现在从单机角度来看貌似可以了,这时候我们应该从服务器角度去思考问题了,在高并发的场景下,一台服务器肯定是i不够的,这时候可能是多台服务器来支撑,我们再看下刚才的代码,肯定又出问题了,线程数允许1只是对当前服务器而言,多台服务器的时候等于没有锁。

而redis可以很轻松的支持分布式锁

setnx(key,value)

key值如果存在,返回false,这样多台服务器也能保证 锁住

public class TestController : Controller
    {
       
        private StackRedis _redis = StackRedis.Current;
        ILogger<TestController> logger;

        public TestController(ILogger<TestController> logger)
        {
            this.logger = logger;
        }
        // GET: api/<controller>
        [HttpGet]
        public async Task<IEnumerable<string>> GetAsync()
        {
           
            try
            {
                if(_redis.SetNX(key,value))
                {

                 int number = Convert.ToInt32(await _redis.Get("number"));
                if (number > 0)
                {
                    number--;
                }
                else
                {
                    logger.LogInformation("没有库存");
                }
                logger.LogInformation(number + "");
                await _redis.Set("number", number.ToString());
                return new string[] { "value1", "value2" };

                }
               
            }
            finally
            {
                
                _redis.DelNX(key)
            }


        }

       
    }
}

当执行时间太长的时候,我们需要加上这把锁的过期时间10s,不然也有可能造成程序死在那里。

但是加上过期时间之后假设第一个线程用执行完需要15秒,第二个线程执行完需要5秒,所以第一个线程执行一大半的时候,所就被释放掉了,这时候第二个人线程就可进来了,但是第一个线程还没执行结束,它又执行了代码中的释放锁,但是这把锁其实并不是线程一的锁,是线程二的锁,这时候线程3可以进来,如此往复,最终这把分布式锁等于没有,所以我们需要在加入个guid或者雪花算法,来保证删除的锁是自己加的锁。

为了保证过期时间的合适,我们同时可以优化,开启分线程,做定时器,每隔一段时间对锁的过期时间进行延期,重新设置为10s,主线程执行完,分线程同时被杀掉。

另外如果多台redis,或者是主从redis,一台主的redis突然断电,然而在主redis的锁还没有同步到另一个redis,但是程序已经执行了,这样会造成上面同样的问题,又把别人的锁删掉了,高并发场景下,锁又等于没了,所以我们在主从redis下,需要将所有的redis同步到锁以后,才认为已经上了锁