Redis 分布式锁的实现

0X00 测试环境

CentOS 6.6 + Redis 3.2.10 + PHP 7.0.7(+ phpredis 4.1.0)

[[email protected] ~]# cat /etc/issue
CentOS release 6.6 (Final)
Kernel \r on an \m

[[email protected] ~]# redis-server -v
Redis server v=3.2.10 sha=00000000:0 malloc=jemalloc-3.6.0 bits=32 build=8903a4502b3c9f88
[[email protected] ~]# php -v
PHP 7.0.7 (cli) (built: Feb 11 2017 16:47:30) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

0X01 什么是分布式锁

redis 官网上对分布式锁的描述(https://redis.io/topics/distlock)是:

Distributed locks are a very useful primitive in many environments where different processes must operate with shared resources in a mutually exclusive way.

即在很多环境中,分布式锁是一种非常有用的原语,它使不同的进程必须以 互斥 的方式操作共享资源。

0x02 为什么要使用分布式锁

Redis 是 单线程(single-threaded)的内存数据结构存储,因此 Redis 所有的 基础命令 都是 原子性 的。但是 多个连贯的命令 在 高并发 的情况下数据的 一致性 就不能得到保障,数据很可能会被其他的客户端修改。

举一个并发情况下没有使用锁的例子:

例1. without_lock.php

<?php
$redis  = new Redis();
$redis->connect(‘localhost‘, 6379);

echo date(‘Y-m-d H:i:s‘, time()).‘ start‘.PHP_EOL;

for($i = 0; $i < 100000; $i++)
{
        $count  = (int)$redis->get(‘key‘);
        $count  += 1;
        $redis->set(‘key‘, $count);

        usleep(0.01);
}

echo date(‘Y-m-d H:i:s‘, time()).‘ end‘.PHP_EOL;

在两个客户端中同时执行该程序,如果不考虑并发情况下数据的一致性,那么两个客户端执行完之后,键 key 的值应该是 200000,但是实际的结果小于 200000。

客户端1:

客户端2:

结果:

原因是:

get、值+1、set 这三个操作不是原子操作,在两个客户端同时执行脚本的时候,哪一个客户端先到就先执行哪个客户端的命令。例如:

a.当前 key 的值是 10;

b.客户端1 取出 key 的值是 10,此时客户端2 的命令也到了,取出 key 的值是 10;

c.客户端1 把值加 1,存入 key,此时 key 的值是  11;

d.客户端2 把值加 1,存入 key,此时 key 的值还是 11

所以在并发情况下最终 key 的值会小于希望的值。

0x03 Redis 的事务能不能解决并发下数据一致性问题

redis 中有一系列事务(https://redis.io/topics/transactions)相关的命令,包括 watch、multi、exec 等。能不能使用这些命令保证并发情况下的数据一致性呢。

根据 redis 官网的介绍,redis 可以使用 check-and-set(CAS)实现 乐观锁(Optimistic),以下是官网给出的示例:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

使用 watch 命令监视键 mykey,如果在 exec 命令执行之前,其他的客户端修改了 mykey 的值时,整个事务就会 终止,并且 exec 命令会返回 null 通知事务失败 —— 根据官网的说明,在接到事务失败的情况下,只需要重复执行上述操作,并且希望不会有新的竞态情况发生,这种形式的锁被称为乐观锁。如果把事务应用在例1 中,结果很可能会出现大量的事务失败,而并不能达到希望的结果,即最终 key 的值是 200000。

例2. whith_watch.php

<?php
$redis  = new Redis();
$redis->connect(‘localhost‘, 6379);

echo date(‘Y-m-d H:i:s‘, time()).‘ start‘.PHP_EOL;

$falseCount = 0;
for($i = 0; $i < 100000; $i++)
{
        $redis->watch(‘key1‘);
        $count  = (int)$redis->get(‘key1‘);
        $count  += 1;

        $ret = $redis->multi()
                ->set(‘key1‘, $count)
                ->exec();

        if(false === $ret)
        {
                $falseCount += 1;
        }
}

echo "falseCount:{$falseCount}".PHP_EOL;
echo date(‘Y-m-d H:i:s‘, time()).‘ end‘.PHP_EOL;

客户端1:

事务失败了 33043 次

客户端2:

事务失败了 63228 次

结果:

结果也是远远小于 200000

说明:

以上为什么不能写成

for($i=0; $i < 100000; $i++)
{
    $redis->watch(‘key1‘);
    $redis->multi();
    $count = (int)$redis->get(‘key1‘);
    $count += 1;
    $redis->set(‘key1‘, $count);
    $redis->exec();
}

参考 phpredis 文档 https://github.com/phpredis/phpredis/#multi

multi() returns the Redis instance and enters multi-mode. Once in multi-mode, all subsequent method calls return the same object until exec() is called.

即在 multi-mode 下,所有的后续方法都返回同一个对象(Redis Object),直到调用 exec命令。也就是说在 multi-mode 下,任何的命令都不会真正执行,而是会返回 Redis Object,直到调用 exec 命令,才真正执行事务中的每一条命令,因此

$count = (int)$redis->get(‘key1‘);

上述代码中的 get 命令,并没有真正执行,该语句实际只会返回一个 Redis 对象。

0x04 分布式锁的实现流程

基本思路是:

a.一个进程(客户端)去获取锁,如果可以获取到,则写入锁并且设置锁的有效期,当数据处理完之后,释放该锁;

b.当获取锁失败时,判断锁是否存在有效期,如果不存在,则设置锁的有效期,超出有效期后锁会自动释放

c.当数据处理完后,释放锁时,需要判断锁是否是其他进程(客户端)的锁,如果不是则释放,如果是则跳过

分布式锁需要注意的问题包括:

a. 防止持有锁的进程(客户端)意外崩溃,导致锁得不到释放,形成死锁,其他进程(客户端)一直得不到该锁;

b.防止持有锁的进程(客户端)因为操作时间过长(超过了锁的有效期)导致锁自动释放,最后到了该释放锁的时候却错误的释放了其他进程(客户端)的锁;

c.防止一个进程(客户端)的锁过期后,其他多个进程(客户端)同时尝试获取锁,并且都获取成功了

流程图:

0x05 Redis 实现分布式锁

要在高并发下消除竞争、保证数据一致性,可以采用 Redis 的分布式锁来实现。

有两种实现方式:

第一种是低于 2.6.12 版本的redis,需要使用 setnx、expire、ttl 等命令组合使用;

第二种是 2.6.12 版本起,redis 给 set 命令(https://redis.io/commands/set) 提供了更丰富的参数,来代替以上的几个命令

SET key value [EX seconds] [NX]

其中可选参数 EX seconds 表示键的过期时间为 seconds 秒,NX 表示只有键不存在时才对键进行设置,这个命令可以替代 setNx 命令加上 expire 命令,而且它是原子性的。

1.Redis Version < 2.6.12

例3.lock.php

<?php
/**
 * redis 分布式锁
**/

class Lock
{
        private $redis = ‘‘;

        public function __construct($host, $port = 6379)
        {
                $this->redis = new Redis();
                $this->redis->connect($host, $port);
        }

        // 加锁
        public function getLock($lockName, $timeout = 2)
        {
                $identifier     = uniqid();
                $timeout        = ceil($timeout);
                $end            = time() + $timeout;

                while(time() < $end)
                {
                        if($this->redis->setnx($lockName, $identifier))
                        {
                                $this->redis->expire($lockName, $timeout);

                                return $identifier;
                        }
                        elseif($this->redis->ttl($lockName) == -1)
                        {
                                $this->redis->expire($lockName, $timeout);
                        }
                        usleep(0.001);
                }

                return false;
        }

        // 释放锁
        public function releaseLock($lockName, $identifier)
        {
                if($this->redis->get($lockName) == $identifier)
                {
                        $this->redis->multi();
                        $this->redis->del($lockName);
                        $this->redis->exec();

                        return true;
                }

                return false;
        }

        // test
        public function test($lockName)
        {
                echo date(‘Y-m-d H:i:s‘, time()).‘ start‘.PHP_EOL;

                for($i = 0; $i < 100000; $i++)
                {
                        $identifier = $this->getLock($lockName);
                        if($identifier)
                        {
                                $count  = $this->redis->get(‘count‘);
                                $count  = intval($count) + 1;
                                $this->redis->set(‘count‘, $count);
                                $this->releaseLock($lockName, $identifier);
                        }
                }

                echo date(‘Y-m-d H:i:s‘, time()).‘ end‘.PHP_EOL;
        }
}

$obj = new Lock(‘localhost‘);
$obj->test(‘lock_name‘);

说明:代码参考 《Redis构建分布式锁

客户端1:

客户端2:

结果:

2.Redis Version >= 2.6.12

例4.with_set.php

把上例中的

if($this->redis->setnx($lockName, $identifier))
{
        $this->redis->expire($lockName, $timeout);

        return $identifier;
}
elseif($this->redis->ttl($lockName) == -1)
{
        $this->redis->expire($lockName, $timeout);
}

替换为:

if($this->redis->set($lockName, $identifier, [‘nx‘, ‘ex‘=>intval($timeout)]))
{
        return $identifier;
}

即可。

以上使用 Redis 实现了分布式锁。

0x06 参考

1.《Redis构建分布式锁》

3.《在 Redis 上实现的分布式锁》

原文地址:https://www.cnblogs.com/dee0912/p/9338976.html

时间: 2024-10-03 17:04:45

Redis 分布式锁的实现的相关文章

Redis分布式锁实现

直接上代码: 1 package cn.wywk.yac.comm.redis; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import redis.clients.jedis.Jedis; 7 8 /** 9 * ClassName: redis分布式锁实现 <br/> 10 * date: 2017年2月17日 上午10:23:24 <br/> 11 * 12 * @author 134

redis分布式锁和消息队列

最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP等,不能在内存中使用锁,或者如Java这样的,需要一下更为简单的锁校验的时候,redis分布式锁的使用就足够满足了.redis的分布式锁其实就是基于setnx方法和redis对key可设置有效时间的功能来实现的.基本用法比较简单. public boolean tryLock(String lock

RedLock.Net - 基于Redis分布式锁的开源实现

工作中,经常会遇到分布式环境中资源访问冲突问题,比如商城的库存数量处理,或者某个事件的原子性操作,都需要确保某个时间段内只有一个线程在访问或处理资源. 因此现在网上也有很多的分布式锁的解决方案,有数据库.MemCache.ZoopKeeper等等的方式. 这次,我们要学习的是一个基于Redis分布式锁的插件,RedLock.Net. 首先必须要有一个Redis服务来支持此分布式锁,其次就当然是要获取此插件了. 可以从Nuget中获取,也可以直接去Github下载   https://github

redis分布式锁小试

一.场景 项目A监听mq中的其他项目的部署消息(包括push_seq, status, environment,timestamp等),然后将部署消息同步到数据库中(项目X在对应环境[environment]上部署的push_seq[项目X的版本]).那么问题来了,mq中加入包含了两个部署消息 dm1 和 dm2,dm2的push_seq > dm1的push_seq,在分布式的情况下,dm1 和 dm2可能会分别被消费(也就是并行),那么在同步数据库的时候可能会发生 dm1 的数据保存 后于

Memcached 和 Redis 分布式锁方案

分布式缓存,能解决单台服务器内存不能无限扩张的瓶颈.在分布式缓存的应用中,会遇到多个客户端同时争用的问题.这个时候,需要用到分布式锁,得到锁的客户端才有操作权限. Memcached 和 Redis 是常用的分布式缓存构建方案,下面列举下基于Memcached 和 Redis 分布式锁的实现方法. Memcached 分布式锁 Memcached 可以使用 add 命令,该命令只有KEY不存在时,才进行添加,或者不会处理.Memcached 所有命令都是原子性的,并发下add 同一个KEY ,只

spring boot redis分布式锁

随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring boot starter,方便使用 一. Redis 分布式锁的实现以及存在的问题 锁是针对某个资源,保证其访问的互斥性,在实际使用当中,这个资源一般是一个字符串.使用 Redis 实现锁,主要是将资源放到 Redis 当中,利用其原子性,当其他线程访问时,如果 Redis 中已经存在这个资源,就不允

Redis分布式锁解决抢购问题

首先分享一个业务场景-抢购.一个典型的高并发问题,所需的最关键字段就是库存,在高并发的情况下每次都去数据库查询显然是不合适的,因此把库存信息存入Redis中,利用redis的锁机制来控制并发访问,是一个不错的解决方案. 首先是一段业务代码: @Transactional public void orderProductMockDiffUser(String productId){ //1.查库存 int stockNum = stock.get(productId); if(stocknum =

Redis分布式锁的try-with-resources实现

Redis分布式锁的try-with-resources实现 一.简介 在当今这个时代,单体应用(standalone)已经很少了,java提供的synchronized已经不能满足需求,大家自然 而然的想到了分布式锁.谈到分布式锁,比较流行的方法有3中: 基于数据库实现的 基于redis实现的 基于zookeeper实现的 今天我们重点说一下基于redis的分布式锁,redis分布式锁的实现我们可以参照redis的官方文档. 实现Redis分布式锁的最简单的方法就是在Redis中创建一个key

Redis分布式锁

转自:https://www.cnblogs.com/linjiqin/p/8003838.html 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现

Redis分布式锁的正确实现方式

前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁. 可靠性 首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性.在任意时刻,只有一个客户端能持有锁. 不会发生死锁.即使有一个客户端在