redis分布式锁,以及和ZooKeeper分布式锁的性能对比

redis连接池代码略过

实现类并没有使用lua,而是使用了JDK自带的System.currentTimeMillis(),因此要求分布式各台服务器直接时间同步,否则会出现超时时间错误地相互覆盖问题

redis实现类
import org.apache.log4j.Logger;
import redis.clients.jedis.Jedis;

/**
 * redis分布式锁
 */
public class RedisLock {
    private static Logger logger = Logger.getLogger(RedisLock.class);

    /**
     * 锁等待时间,防止线程饥饿
     */
    private int TIMEOUT_MSECS = 10 * 1000;

    /**
     * 锁超时时间,防止线程在入锁以后,无限的执行等待
     */
    private int EXPIRE_MSECS = 60 * 1000;

    private static int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    /**
     * Lock key path.
     */
    private String lockKey;

    /**
     * Java本地与redis对应的锁
     */
    private volatile boolean locked = false;

    private static Jedis jedis;

    /**
     * 构造器
     * 默认锁等待时间10*1000ms,锁超时时间60*1000ms
     * @param jedis jedis连接池
     * @param lockKey   锁的key名称
     */
    public RedisLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * 构造器
     * 默认锁超时时间60*1000ms
     * @param jedis jedis连接池
     * @param lockKey   锁的key名称
     * @param timeoutMsecs  锁超时时间
     */
    public RedisLock(Jedis jedis, String lockKey, int timeoutMsecs) {
        this(jedis, lockKey);
        this.TIMEOUT_MSECS = timeoutMsecs;
    }

    /**
     * 默认构造器
     * @param jedis
     * @param lockKey
     * @param timeoutMsecs
     * @param expireMsecs
     */
    public RedisLock(Jedis jedis, String lockKey, int timeoutMsecs, int expireMsecs) {
        this(jedis, lockKey, timeoutMsecs);
        this.EXPIRE_MSECS = expireMsecs;
    }

    /**
     * @return lock key
     */
    public String getLockKey() {
        return lockKey;
    }

    /**
     * 根据key获取值
     *
     * @param key
     * @return
     */
    private String get(final String key) {
        Object obj = null;
        try {
            obj = jedis.get(key);
        } catch (Throwable e) {

        }
        return obj != null ? obj.toString() : null;
    }

    /**
     * setNX
     * @param key
     * @param value
     * @return
     */
    private boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = jedis.setnx(key, value);
        } catch (Throwable e) {
            logger.error(String.format("setNX redis error, key : %s", key), e);
        }
        return obj != null ? (Boolean) obj : false;
    }

    /**
     * getSet
     * @param key
     * @param value
     * @return
     */
    private String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = jedis.getSet(key, value);
        } catch (Throwable e) {
            logger.error(String.format("getSet redis error, key : %s", key), e);
        }
        return obj != null ? (String) obj : null;
    }

    /**
     * 获得 lock.
     * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
     * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
     * 执行过程:
     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = TIMEOUT_MSECS;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + EXPIRE_MSECS + 1;
            String expiresStr = String.valueOf(expires); //锁到期时间
            if (this.setNX(lockKey, expiresStr)) {
                // lock acquired
                locked = true;
                return true;
            }

            String currentValueStr = this.get(lockKey); //redis里的时间
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
                // lock is expired

                String oldValueStr = this.getSet(lockKey, expiresStr);
                //获取上一个锁到期时间,并设置现在的锁到期时间,
                //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    //防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受

                    //[分布式的情况下]:如果这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
                    // lock acquired
                    locked = true;
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
                延迟100 毫秒,  这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
                只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
                使用随机的等待时间可以一定程度上保证公平性
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

        }
        return false;
    }

    /**
     * Acqurired lock release.
     */
    public synchronized void unlock() {
        if (locked) {
            jedis.del(lockKey);
            locked = false;
        }
    }
}

测试类
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

/**
 * 测试类
 */
public class main {
    public static void main(String[] args) {
        final RedisLock redisLock = new RedisLock(RedisUtil.getJedis(), "test");
        final CountDownLatch down = new CountDownLatch(1);
        for (int i = 0; i < 30; i++) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        down.await();
                        redisLock.lock();
                    } catch (Exception e) {
                    }
                    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
                    String orederNo = sdf.format(new Date());
                    System.out.println("生成的订单号为:" + orederNo);
                    try {
                        redisLock.unlock();
                    } catch (Exception e) {
                    }
                }
            }).start();
        }
        down.countDown();
    }
}
测试结果

[QC] INFO [main] RedisUtil.getJedis(77) | init Redis success
生成的订单号为:15:51:44|643
生成的订单号为:15:51:44|643
生成的订单号为:15:51:44|643
生成的订单号为:15:51:44|643
生成的订单号为:15:51:44|643
生成的订单号为:15:51:44|643
生成的订单号为:15:51:44|648
生成的订单号为:15:51:44|655
生成的订单号为:15:51:44|657
生成的订单号为:15:51:44|659
生成的订单号为:15:51:44|661
生成的订单号为:15:51:44|664
生成的订单号为:15:51:44|665
生成的订单号为:15:51:44|669
生成的订单号为:15:51:44|676
生成的订单号为:15:51:44|678
生成的订单号为:15:51:44|680
生成的订单号为:15:51:44|686
生成的订单号为:15:51:44|688
生成的订单号为:15:51:44|690
生成的订单号为:15:51:44|693
生成的订单号为:15:51:44|695
生成的订单号为:15:51:44|697
生成的订单号为:15:51:44|699
生成的订单号为:15:51:44|702
生成的订单号为:15:51:44|713
生成的订单号为:15:51:44|714
生成的订单号为:15:51:44|723
生成的订单号为:15:51:44|725
生成的订单号为:15:51:44|729
Process finished with exit code 0

对比之下,ZooKeeper的测试类
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
 
 
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
 
 
/**
* curator分布式锁实现
*/
public class Recipes_lock {
    static String lock_path = "/curator_recipes_lock_path";
    static CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();
 
 
    public static void main(String[] args) throws Exception {
        client.start();
        final InterProcessMutex lock = new InterProcessMutex(client, lock_path);
        final CountDownLatch down = new CountDownLatch(1);
        for (int i = 0; i < 30; i++) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        down.await();
                        lock.acquire();
                    } catch (Exception e) {
                    }
                    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
                    String orederNo = sdf.format(new Date());
                    System.out.println("生成的订单号为:" + orederNo);
                    try {
                        lock.release();
                    } catch (Exception e) {
                    }
                }
            }).start();
        }
        down.countDown();
    }
}
ZooKeeper的测试结果
生成的订单号为:15:32:33|728
生成的订单号为:15:32:33|781
生成的订单号为:15:32:33|849
生成的订单号为:15:32:33|879
生成的订单号为:15:32:33|897
生成的订单号为:15:32:33|972
生成的订单号为:15:32:34|013
生成的订单号为:15:32:34|057
生成的订单号为:15:32:34|130
生成的订单号为:15:32:34|197
生成的订单号为:15:32:34|232
生成的订单号为:15:32:34|282
生成的订单号为:15:32:34|367
生成的订单号为:15:32:34|404
生成的订单号为:15:32:34|425
生成的订单号为:15:32:34|461
生成的订单号为:15:32:34|482
生成的订单号为:15:32:34|515
生成的订单号为:15:32:34|550
生成的订单号为:15:32:34|573
生成的订单号为:15:32:34|600
生成的订单号为:15:32:34|624
生成的订单号为:15:32:34|647
生成的订单号为:15:32:34|670
生成的订单号为:15:32:34|715
生成的订单号为:15:32:34|752
生成的订单号为:15:32:34|774
生成的订单号为:15:32:34|793
生成的订单号为:15:32:34|811
生成的订单号为:15:32:34|836

对比两个结果来看

redis

生成的订单号为:15:51:44|643
...
生成的订单号为:15:51:44|729

ZooKeeper

生成的订单号为:15:32:33|728
...
生成的订单号为:15:32:34|836

结论:相对于ZooKeeper提供的分布式锁来看,redis实现的分布式锁响应更快,对并发的支持性能更好
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342