Redis实现分布式锁

Redis实现分布式锁

分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。 举个不太恰当的例子:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推。

实现原理:

  • 互斥性

    • 保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作
  • 安全性

    • 只有加锁的服务才能有解锁权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁那分布式锁就没啥意义了
    • 可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候忽然a持有的锁过期了,然后b去获得锁,因为a锁过期,b拿到锁,这时候a继续执行第二步进行解锁如果不加校验,就将b持有的锁就给删除了
  • 避免死锁

    • 出现死锁就会导致后续的任何服务都拿不到锁,不能再对共享资源进行任何操作了
  • 保证加锁与解锁操作是原子性操作

    • 这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁
    • 假设加锁操作,操作步骤分为两步:
    • 1,设置key set(key,value)2,给key设置过期时间
    • 假设现在a刚实现set后,程序崩了就导致了没给key设置过期时间就导致key一直存在就发生了死锁

参考:

https://www.cnblogs.com/cmyxn/p/9047848.html(java)

https://www.cnblogs.com/syhx/p/9753433.html(PHP)

redis分布式锁的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
/**
* 实现Redis分布锁
*/
$key = 'test'; //要更新信息的缓存KEY
$lockKey = 'lock:'.$key; //设置锁KEY
$lockExpire = 10; //设置锁的有效期为10秒

//获取缓存信息
$result = $redis->get($key);
//判断缓存中是否有数据
if(empty($result))
{
$status = TRUE;
while ($status)
{
//设置锁值为当前时间戳 + 有效期
$lockValue = time() + $lockExpire;
/**
* 创建锁
* 试图以$lockKey为key创建一个缓存,value值为当前时间戳
* 由于setnx()函数只有在不存在当前key的缓存时才会创建成功
* 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了
* @var [type]
*/
$lock = $redis->setnx($lockKey, $lockValue);
/**
* 满足两个条件中的一个即可进行操作
* 1、上面一步创建锁成功;
* 2、 1)判断锁的值(时间戳)是否小于当前时间 $redis->get()
* 2)同时给锁设置新值成功 $redis->getset()
*/
if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))//存在锁||(当前值<当前时间&&旧值<当前时间)
{
//给锁设置生存时间
$redis->expire($lockKey, $lockExpire);
//******************************
//此处执行插入、更新缓存操作...
//******************************

//以上程序走完删除锁
//检测锁是否过期,过期锁没必要删除,redis自动删除
if($redis->ttl($lockKey))
$redis->del($lockKey);
$status = FALSE;
}else{
/**
* 如果存在有效锁这里做相应处理
* 等待当前操作完成再执行此次请求
* 直接返回
*/
sleep(2);//等待2秒后再尝试执行操作
}
}
}

参考:https://blog.csdn.net/panjiapengfly/article/details/84758971
redis补充知识

setnx(key, value),含义就是SET if Not Exists;如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。

msetnx()所有给定 key 都不存在时,同时设置一个或多个 key-value 对。成功,返回 1 ,失败(至少有一个 key 已经存在),那么返回 0 。

setex(key,seconds,value)为set和expir的结合,关联值和设置生存时间两个动作会在同一时间内完成。

setrange(key_name,8,’yy‘)将原 key_name对应的值第8位后开始替换为 yy子字符串

mset()一次设置多个key 的值,成功返回 ok 表示所有的值都设置了,失败返回 0 表示没有任何值被设置。

例如:>mset key1 HongWan1 key2 HongWan2

get(key) 获取key的值,如果存在,则返回;如果不存在,则返回nil。

getset(key, newValue)该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。 假设key原来是不存在的,返回nil

expire(key_name ,time_in_seconds)命令设置一个键的生存时间,到时间后redis会自动删除它 (单位/秒)

ttl(key_name) 以秒为单位返回 key 的剩余过期时间。当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1

del(key_name) 用于删除已存在的键。不存在的 key 会被忽略。

抢红包高并发解决方案

1、悲观锁,用sql+for update

当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候, CPU 就会将这些得不到资源的线程挂起,挂起的线程也会消耗CPU 的资源,尤其是在高井发的请求中。一旦线程l 提交了事务,那么锁就会被释放,这个时候被挂起的线程就会开始竞争资源,那么竞争到的线程就会被CPU 恢复到运行状态,继续运行。使用悲观锁就会造成大量的线程被挂起和恢复,造成性能下降。

2、乐观锁(非阻塞锁)

是一种不会阻塞其他线程并发的机制,它不使用数据库的锁进行实现,一般用版本号机制。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。

但由于版本不一致的问题,会存在大量红包争抢失败的问题。加入重入机制,提高抢红包的成功率。使用时间戳或者规定抢红包次数

3、redis分布式锁

采用缓存技术,利用Redis的轻量级、便捷、快速的机制解决高并发问题。但是如何解决数据不一致带来的超发问题呢?用分布式锁,缓存中存入一个值(key-value),谁拿到这个值谁就可以执行代码。当信息存入缓存、库存-1之后,我们释放锁。为了防止死锁的发生,可以设置锁的过期时间来解决。

参考:https://blog.csdn.net/qq_33764491/article/details/81083644