nx set 怎么实现的原子性_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 gaoyuanOK127.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 NXOK// 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=0i=0i=0i=3i=3i=3i=4i=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) 

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

释放方法一:

/** * 释放锁 - 利用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 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=0i=0i=0i=0i=0i=0...

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

i=0i=0i=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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值