文档章节

Redis应用之分布式锁(set)

GMarshal
 GMarshal
发布于 09/21 13:20
字数 1358
阅读 357
收藏 14

Redis应用之分布式锁(set)

在单机应用的场景下,我们常使用的锁主要是synchronized与Lock;但是在分布式横行的大环境下,显然仅仅这两种锁已经无法满足我们的需求;

需求:秒杀场景下,有若干服务实例,假设有2个,那么分别会有若干请求分别请求这2个服务实例。要求只能有一个请求秒杀成功,本质是秒杀方法在同一时间内只能被同一个线程执行,这就需要使用到分布式锁。

场景分布式锁

  • 基于数据库实现
    • 基于数据库实现分布式锁,主要使用InnoDB下的for update(如使用行级锁,需加唯一索引)
  • 基于Zookeeper实现
    • 在指定节点的目录下,创建一个唯一的瞬时有序节点。可以使用Curator去实现。
  • 基于缓存实现(redis)
    • 主要使用set(setnx用法有缺陷且过时)

详解redis的set命令

我们已知道set用于设置String类型的key/value值,如下:

127.0.0.1:6379> set name gaoyuan
OK
127.0.0.1:6379> get name
"gaoyuan"
setnx + expire = 非原子性

在redis2.6.12版本之前,分布式锁常使用setnx来实现。setnx是set if not exists的意思,也就是当值不存在时,才可以创建成功,这样就能保证在同一时间只能有个设置成功。

但是,setnx无法在插入值的同时设置超时时间,setnx 与 expire 是两条独立的语句,这样加锁操作就是非原子性的,那么就会带来问题。(比如,当setnx成功后,准备执行expire前,程序突然出现错误,则添加的数据就无法清除了,因为没有超时时间,不会自动清除)

set key value [EX seconds] [PX milliseconds] [NX|XX]

在redis2.6.12版本之后,redis支持通过set在设置值得同时设置超时时间,此操作是原子操作。

// 设置lock的值为123,存在6秒
127.0.0.1:6379> set lock 123 EX 6 NX
OK
// 6秒内,重复设置lock的值为123,返回nil(也就是null)
127.0.0.1:6379> set lock 123 EX 6 NX
(nil)
// 6秒内,获取值,能够获取到
127.0.0.1:6379> get lock
"123"
// 6秒后,获取值,获取为nil,又可以重新set值了
127.0.0.1:6379> get lock
(nil)

下面我们利用set的特性来实现分布式锁。

实现分布式锁

我们先看一个不加锁的例子

我们先构造一个对象 MyThread

class MyThread implements Runnable{
    int i = 0;

    @Override
    public void run() {
        try {
            for(int j=0;j<10;j++){
                i = i + 1;
                // 这里延时,为了让其他线程进行干扰
                TimeUnit.MILLISECONDS.sleep(10);
                i = i - 1;
                System.out.println("i=" + i);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

执行

ExecutorService executorService = Executors.newFixedThreadPool(3);
MyThread myThread = new MyThread();
executorService.submit(myThread);
executorService.submit(myThread);
executorService.submit(myThread);
executorService.shutdown();

输出

i=0
i=0
i=0
i=3
i=3
i=3
i=4
i=4
...

可以看出,i居然会出现不等于0的情况。

Redis加锁(set命令)

获取锁的方法

/**
 * 获取锁
 * 利用set key value [EX seconds] [PX milliseconds] [NX|XX] 命令实现锁机制
 * @author GaoYuan
 */
public static String tryLock(Jedis jedis, int timeout) throws Exception{
    if(timeout == 0){
        timeout = 5000;
    }
    String returnId = null;
    // 生成随机标识
    String id = UUID.randomUUID().toString();
    // 设置锁超时10秒
    int lockExpireMs = 10000;
    long startTime = System.currentTimeMillis();
    // 超时时间内循环获取
    while ((System.currentTimeMillis() - startTime) < timeout){
        String result = jedis.set(lockKey, id, "NX", "PX", lockExpireMs);
        if(result != null){
            returnId = id;
            break;
        }
        TimeUnit.MILLISECONDS.sleep(100);
    }
    if(returnId == null){
        // 获取锁超时,抛出异常
        throw new Exception("获取锁超时");
    }
    // 将set的值返回,用于后续的解锁
    return returnId;
}

释放锁的方法(释放锁的方式有两种)

释放方法一:

/**
 * 释放锁 - 利用redis的watch + del
 * @author GaoYuan
 */
public static boolean unLock(Jedis jedis, String id){
    boolean result = false;
    while(true){
        if(jedis.get(lockKey) == null){
            return false;
        }
        // 配置监听
        jedis.watch(lockKey);
        // 这里确保是加锁者进行解锁
        if(id!=null && id.equals(jedis.get(lockKey))){
            Transaction transaction = jedis.multi();
            transaction.del(lockKey);
            List<Object> results = transaction.exec();
            if(results == null){
                continue;
            }
            result = true;
        }
        // 释放监听
        jedis.unwatch();
        break;
    }
    return result;
}

释放方法二:

/**
 * 释放锁 - 利用lua脚本
 * @author GaoYuan
 */
public static boolean unLockByLua(Jedis jedis, String id){
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(id));
    if (Objects.equals(1, result)) {
        return true;
    }
    return  false;
}

改造之前的例子

class MyThread implements Runnable{
    int i = 0;

    @Override
    public void run() {
        try {
            for(int j=0;j<10;j++){
                Jedis jedis = new Jedis(JedisConfig.HOST, JedisConfig.PORT);
                try {
                    // 尝试获取锁,有超时时间
                    String id = RedisLock.tryLock(jedis,5000);
                    i = i + 1;
                    // 这里延时,为了让其他线程进行干扰(当然,加锁就不会有干扰)
                    TimeUnit.MILLISECONDS.sleep(10);
                    i = i - 1;
                    // 加锁后,期望值 i=0
                    System.out.println("i=" + i);
                    // 释放锁
                    RedisLock.unLock(jedis, id);
                }catch (Exception e){
                    // e.printStackTrace();
                    System.out.println("获取锁超时");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行输出

i=0
i=0
i=0
i=0
i=0
i=0
...

将run方法中的延时时间设置成1秒(1000)后,会打印超时的情况

i=0
i=0
i=0
获取锁超时
获取锁超时
i=0
...

至此利用jedis实现了分布式锁。

码云

完整代码见: https://gitee.com/gmarshal/foruo-demo/tree/master/foruo-demo-redis/foruo-demo-redis-lock

博客

开源中国博客地址

https://my.oschina.net/gmarshal/blog/2120428

个人博客地址

http://blog.foruo.top

欢迎关注我的个人微信订阅号:(据说这个头像程序猿专用)

输入图片说明

© 著作权归作者所有

共有 人打赏支持
上一篇: 源码模仿之RPC
GMarshal
粉丝 19
博文 58
码字总数 46067
作品 0
南京
程序员
私信 提问
Redis常见的应用场景解析

Redis是一个key-value存储系统,现在在各种系统中的使用越来越多,大部分情况下是因为其高性能的特性,被当做缓存使用,这里介绍下Redis经常遇到的使用场景。 Redis特性 一个产品的使用场景肯...

IT米粉
2017/09/25
0
0
Spring-data-redis + redis 分布式锁(二)

分布式锁的解决方式 基于数据库表做乐观锁,用于分布式锁。(适用于小并发) 使用memcached的add()方法,用于分布式锁。 使用memcached的cas()方法,用于分布式锁。(不常用) 使用redis的setnx...

xiaolyuh
2017/11/16
0
0
jedisLock—redis分布式锁实现

搬运工:http://www.cnblogs.com/0201zcr/p/5942748.html jedisLock—redis分布式锁实现 一、使用分布式锁要满足的几个条件: 系统是一个分布式系统(关键是分布式,单机的可以使用Reentrant...

北极之北
2017/01/06
1K
2
并发编程-锁的发展和主流分布式锁比较总结

一、锁的发展 系统结构由传统的“单应用服务--》SOA --》微服务 --》无服务器” 的演进过程中,场景越来越复杂,由单体应用的但进程中多线程并发的内存锁,随着互联网场景越来越复杂,在复杂...

贾浩v
2017/10/24
0
0
redis分布式锁

关于分布式锁的概念网上太多了,这里就不罗嗦了。对于开发者来说,最关心的应该是什么情况下使用分布式锁。 使用分布式锁,一般要满足以下几个条件: · 分布式系统(关键是分布式) · 共享资...

明舞
2015/10/16
5.7K
3

没有更多内容

加载失败,请刷新页面

加载更多

MicroStation Developer Shell

REG ADD HKLM\SOFTWARE\Microsoft\VisualStudio\8.0\Setup\VS /v ProductDir /t REG_SZ /d "C:\Program Files (x86)\Microsoft Visual Studio 8\VC\" /reg:32 CALL "C:\Program Files (x86)\......

oready
刚刚
0
0
CURL常用命令

下载单个文件,默认将输出打印到标准输出中(STDOUT)中 curl http://www.centos.org 通过-o/-O选项保存下载的文件到指定的文件中: -o:将文件保存为命令行中指定的文件名的文件中 -O:使用U...

SuShine
5分钟前
0
0
docker搞个wordpress

1.先把wordpress的镜像下载下来 docker pull wordpress 2.下载mysql docker pull mysql:lastest 3.启动mysql docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:t......

无极之岚
16分钟前
0
0
【宇润日常疯测-005】PHP 中的 clone 和 new 性能比较

clone和new本不应该放在一起比较,它们的作用是不同的。但可能有一些场景下,可以用clone也可以用new,那么这时候我们选哪个呢? 我编写了两个测试,第一个是声明一个空类,第二个是带构造方...

宇润
17分钟前
0
1
点击按钮弹出类似IOS 底部 dialog

implementation 'com.baoyz.actionsheet:library:1.1.7' 然后设置按钮点击监听,,调用下列代码即可 ActionSheet.createBuilder(this, getSupportFragmentManager()) ......

lanyu96
20分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部