redis分布式锁

原创 2016年08月29日 16:06:04

背景
在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

Redis命令介绍
使用Redis实现分布式锁,有两个重要函数需要介绍

SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

GETSET命令
语法:
GETSET key value
功能:
将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。

兵贵精,不在多。分布式锁,我们就依靠这四个命令。但在具体实现,还有很多细节,需要仔细斟酌,因为在分布式并发多进程中,任何一点出现差错,都会导致死锁,hold住所有进程。

加锁实现

SETNX 可以直接加锁操作,比如说对某个关键词foo加锁,客户端可以尝试
SETNX foo.lock <current unix time>

如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过
DEL foo.lock

命令来释放锁。
如果返回0,说明foo已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。如果是堵塞调用调用,就需要进入以下个重试循环,直至成功获得锁或者重试超时。理想是美好的,现实是残酷的。仅仅使用SETNX加锁带有竞争条件的,在某些特定的情况会造成死锁错误。

处理死锁

在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,使用GET获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
C2 向foo.lock发送DEL命令。
C2 向foo.lock发送SETNX获取锁。
C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
C3 向foo.lock发送SETNX获取锁。

此时C2和C3都获取了锁,产生竞争条件,如果在更高并发的情况,可能会有更多客户端获取锁。所以,DEL锁的操作,不能直接使用在锁超时的情况下,幸好我们有GETSET方法,假设我们现在有另外一个客户端C4,使用GETSET方式代替SETNX,DEL可以避免这种情况产生。

时间戳的问题

我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。
锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为秒,从加锁到执行操作再到解锁,一般操作肯定都能在一秒内完成。这样的话,我们上面的CASE,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

分布式锁的问题

1:必要的超时机制:获取锁的客户端一旦崩溃,一定要有过期机制,否则其他客户端都降无法获取锁,造成死锁问题。
2:分布式锁,多客户端的时间戳不能保证严格意义的一致性,所以在某些特定因素下,有可能存在锁串的情况。要适度的机制,可以承受小概率的事件产生。
3:只对关键处理节点加锁,良好的习惯是,把相关的资源准备好,比如连接数据库后,调用加锁机制获取锁,直接进行操作,然后释放,尽量减少持有锁的时间。
4:在持有锁期间要不要CHECK锁,如果需要严格依赖锁的状态,最好在关键步骤中做锁的CHECK检查机制,但是根据我们的测试发现,在大并发时,每一次CHECK锁操作,都要消耗掉几个毫秒,而我们的整个持锁处理逻辑才不到10毫秒,玩客没有选择做锁的检查。
5:sleep学问,为了减少对Redis的压力,获取锁尝试时,循环之间一定要做sleep操作。但是sleep时间是多少是门学问。需要根据自己的Redis的QPS,加上持锁处理时间等进行合理计算。

redis分布式锁简单实例:

import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * Redis分布式锁对象
 */
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
public static RedisService redisService;
    // lock flag stored in redis
    private static final String LOCKED = "TRUE";
    private static final String PREFIX = "xmall:lock:";
    // lock expire time(s)
    public static final int EXPIRE = 300;
    // timeout(ms)
    private long timeout = 30000;
    // private Jedis jedis;
    private String key;
    // state flag
    private boolean locked = false;
    private long startTime;


public RedisLock(String key) {
        this.key = PREFIX + key;
    }

public RedisLock(String key, long timeout) {
        this.key = PREFIX + key;
        this.timeout = timeout;
    }

    private void lock(long timeout) {
    this.startTime = System.currentTimeMillis();
        long nano = System.nanoTime();
        timeout *= 1000000;
        final Random r = new Random();
        try {
            while ((System.nanoTime() - nano) < timeout) {
                if (redisService.setnx(key, LOCKED, EXPIRE) > 0) {
                    locked = true;
                    logger.debug("add RedisLock[" + key + "].");
                    break;
                }
                Thread.sleep(5, r.nextInt(500));
            }
            if(!locked){
            logger.info("wait redis[" + key + "] lock timeout.");
            }
        } catch (Exception e) {
        logger.error("throws Exception", e);
        unlock();
        }
    }

    public void unlock() {
        if (locked) {
        redisService.del(key);
            logger.debug("release RedisLock[" + key + "].");
        }
        logger.info("the RedisLock["+this.key+"] has continued {}ms", System.currentTimeMillis()-this.startTime);
    }

    public void lock() {
        lock(timeout);
    }
}

在事务开启时调用lock()方法加锁,事务结束后调用unlock()方法解锁

版权声明:本文为博主原创文章,未经博主允许不得转载。

redis分布式锁

  • 2017年09月30日 09:41
  • 647KB
  • 下载

Redis分布式锁实现原理 java版

Redis因为是单线程的,所以本身没有锁的概念。 所以分布式锁的实现原理是往Redis当中写入一个key(调用方法setnx),写入成功相当于获取锁成功。写入失败也即是setnx方法返回0,获取锁失败...

分布式锁-redis实现(2)

一、前言     对于前面的编写redis锁非常简单,也编写了测试用例,但是用起来是不是感觉毕竟麻烦。今天我们把redis锁,进一步小封装一下,用起来就只有几行代码了。对于没有看文章二的同学可能是个遗...

基于redis集群实现的分布式锁,可用于秒杀商品的库存数量管理,有测试代码(何志雄)

转载请标明出处。 在分布式系统中,经常会出现需要竞争同一资源的情况,本代码基于redis3.0.1+jedis2.7.1实现了分布式锁。 redis集群的搭建,请见我的另外一篇文章:《re...
  • tzszhzx
  • tzszhzx
  • 2015年06月04日 16:19
  • 4288

基于Redis实现简单的分布式锁

在分布式场景下,有很多种情况都需要实现最终一致性。在设计远程上下文的领域事件的时候,为了保证最终一致性,在通过领域事件进行通讯的方式中,可以共享存储(领域模型和消息的持久化数据源),或者做全局XA事务...

redis集群实现分布式锁

好久没写博客了,突然想起来以前看老外博客上redis做分布式锁的想法。 实现一个DLM(distributed lock manager)主要需要考虑一下几个问题: 1. lock server本身需...

redis setnx实现分布式锁

redis在分布式环境下才需要实现锁,一个客户端下不会出现竞争问题 package 使用setnx_getset; import java.io.IOException; import redis....

Redis实现分布式锁原理与实现分析

一、关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到。  我举二个例子:  场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能在某一个时刻会有二笔一样的单子...

用Redis构建分布式锁

《Redis官方文档》用Redis构建分布式锁 原文链接  译者:yy-leo   校对:方腾飞(红体标记重点) 用Redis构建分布式锁 在不同进程需要互斥地访问共享资源时,分布式锁是一种非...

通过Redisson实现基于redis的分布式锁

除了上一篇写的基于Jedis利用redis的setnx函数实现分布式锁之外。redis官方推荐使用Redisson作为分布式锁的首选。使用Redisson,不需要自己去封装lock和unlock方法。...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:redis分布式锁
举报原因:
原因补充:

(最多只允许输入30个字)