redis 分布式锁
锁的来源
多个进程(或者线程)对临界区(资源抢占等)的访问。由于临界区同时只能有一个进程 或者线程运行。
进程在执行时,任何时间点有可能出现时间片用完(产生时间中断),然后被挂起,CPU 执行另外的进程,如果对临界区的访问不加任何限制,导致新的进程也开始访问临界区, 这样就容易出问题。
一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
// 线程一
static int i = 0;
static int t1 = 0;
static int t2 = 0;
static int sig = 0;
void sig_handler(int signum){
sig = signum;
}
void thread_1(void) {
while(sig==0){
i = i + 1;
t1 = t1 + 1;
printf("i=%d t1=%d\n", i, t1);
sleep(1);
}
}
// 线程二
void thread_2(void) {
while(sig==0){
i = i + 1;
t2 = t2 + 1;
printf("i=%d t2=%d\n", i, t2);
sleep(1);
}
}
int main(void) {
pthread_t id_1, id_2;
int ret;
signal(SIGINT, sig_handler);
/*创建线程一*/
ret=pthread_create(&id_1, NULL, (void *) thread_1, NULL);
if(ret != 0) {
printf("Create pthread 1 error!\n");
return -1;
}
/*创建线程二*/
ret=pthread_create(&id_2, NULL, (void *) thread_2, NULL);
if(ret != 0) {
printf("Create pthread 2 error!\n");
return -1;
}
/*等待线程结束*/
pthread_join(id_1, NULL);
pthread_join(id_2, NULL);
return 0;
}
运行到第34秒的时候,结果就出错了,出现了两个i=67。
i=64 t1=32
i=65 t2=33
i=66 t1=33
i=67 t1=34
i=67 t2=34
i=68 t1=35
i=69 t2=35
下面看一个文件 b.c
static int i=0;
void f(){
i = i + 1;
}
用gcc -S 得到汇编代码,可以看到 i = i + 1,被转换成了三条语句。
movl i(%rip), %eax
addl $1, %eax
movl %eax, i(%rip)
执行周期(取指,间址,执行,中断):当第一条执行执行完之后,发生了中断, 这个时候线程1里面 变量i=66, eax=66。
发生了中断,然后开始执行线程2。执行完之后i=67。这个时候有开始执行线程1, 因为线程1的第1条指令已经执行了,所以从第二条指令开始执行,最后i=67。
出现了 两次i=67。
因此,一个进程(或者线程,本质上是一个调度的单位)在访问临界区时,需要锁。
锁的特征
锁有两个接口:上锁,解锁。然后,有一个执行者(就是进程,或者线程)。
上锁的操作必须是排他性的。多个线程同时争夺一把锁时,最多只有一个线程成功。
解锁的操作必须是上锁的线程去解锁。
扩展到分布式
比如电商系统,多个进程同时购买一件商品时,需要将数据库中的数量都减去1等等。
多个进程也可能分布在多台机器上,这个时候就需要用分布式锁。
redis的实现
上锁
必须要用一条指令。由于进程在任何时候可能挂掉,导致锁得不到释放,所以 这个锁要加一个过期时间。
value必须要保证一个唯一性,解锁的时候需要用来判断是锁的获得者在解锁。
set key value expire_seconds NX
解锁
需要用lua脚本保证原子性
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
2021/04/06