redis 高并发场景下的锁的问题
问题预防:预见并防止问题的发生 #生活技巧# #学习技巧# #解决问题的思考技巧#
简单场景:一个下单按钮,调用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;
}
[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同步到锁以后,才认为已经上了锁
网址:redis 高并发场景下的锁的问题 https://www.yuejiaxmz.com/news/view/415900
相关内容
【redis】redis压力测试工具redis压力测试工具
智能门锁能实现哪些生活场景?
Laravel 中使用 swoole 项目实战开发案例二 (后端主动分场景给界面推送消息)life
探索高效物联网解决方案:iot物联网框架
图技术在美团外卖下的场景化应用及探索
5G NB-IoT智能门锁具体能实现生活中的哪些场景的便利性?
Redisson 的 Watch Dog机制
基于人工智能的智慧校园助手(springboot+springcloud+redis+vue+vant ui+element ui+mysql+Elasticsearch+RabbitMQ项目)
物联网海量设备心跳注册,脱网清除——多线程高并发互斥锁落地