redis 高并发场景下的锁的问题

发布时间:2024-12-08 17:15

问题预防:预见并防止问题的发生 #生活技巧# #学习技巧# #解决问题的思考技巧#

简单场景:一个下单按钮,调用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项目)
物联网海量设备心跳注册,脱网清除——多线程高并发互斥锁落地

随便看看