【开发经验】redis和zookeeper分布式锁对比

引言
如果有int count=0;10000个线程都执行count++,执行完之后count的值会是10000吗?

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0 ;i<10000;i++){
            new Thread(()->{
                    count++;
            }).start();
        }
        //等线程执行完
        Thread.sleep(5000l);
        System.out.println(count);
    }
}

多跑几次即可发现,有的时候得到的值不是自己想要的。这里就是多线程安全问题了。在单机的情况下通过jdk自带的synchronized或者ReentrantLock都可以解决如上问题(现在不考虑使用AtomicInteger的情况下)。如果是多机器的情况下呢?jdk自带的锁就无法达到效果了。比如:

    private IUserDao userDao;

    public synchronized void addUser(String mobile,String name){
        Boolean exists = userDao.getUserInfo(mobile);
        if(exists){
            userDao.addUser(mobile,name);
        }
    }

防止用户操作过快,导致重复添加到数据库中,在单机的情况下,通过synchronized可防止用户重复添加。但是如果是多个服务器的情况,用户通过机器刷请求,这个时候jdk自带的synchronized也无法起到互斥的效果。这个时候就需要分布式锁来处理了。
上面的例子不算特别的好,大家脑补一下场景啦。

redis分布式锁

​ 首先必须知道的几个redis命令

redis 命令学习

setnx

SETNX 是【SET if Not eXists】(如果不存在,则 SET)的简写;

当且仅当 key 不存时,将 key 的值设为 value 。若给定的 key 已经存在,则 SETNX 不做任何动作。

返回值:

设置成功,返回 1 。

设置失败,返回 0 。

redis> EXISTS job                # job 不存在
(integer) 0
 
redis> SETNX job "programmer"    # job 设置成功
(integer) 1
 
redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败
(integer) 0
 
redis> GET job                   # 没有被覆盖
"programmer"

set

设置一个值到redis中。

使用setnx命令的时候有一个不足,即是无法在设置一个值的同时设置key的过期时间,只能通过设置成功之后,再通过expire设置过期时间,这个操作无法保证原子性。所以如果想在设置一个值的时候再设置过期时间保证它的原子性,只能通过set命令。如下:

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

EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在返回ok,如果key存在返回(nil)
XX:key存在返回ok,如果key不存在失败返回(nil)

如此高仿setnx命令,当job不存在时设置value为aa,超时时间10秒

> set job aa ex 10 nx
OK
> ttl job
(integer) 7
> set job aa ex 10 nx
(nil)

getset

getset是【GETSET key value】

将给定 key 的值设为 value ,并返回 key 的旧值(old value)。

当 key 存在但不是字符串类型时,返回一个错误。

> exists job
(integer) 0
> getset job aa
(nil)
> set job aa
OK
> getset job bb
"aa"

redis 锁常见思路

通常情况下c0通过setnx进行设置值,如果设置成功,即可认为获取锁成功,但是如果c0一直不释放锁,或者说c0线程异常,释放锁失败,那就会导致其他线程一直无法获取锁。那必须得有异常处理的逻辑,所以思路如下

网上常见的实例:

实例一
实现思路
  1. c0通过setnx设置key为lock,如果设置成功,则通过expire设置key过期时间,获取锁成功;
  2. c1 ,c2(也可能是更多线程) 也同样通过setnx设置key为lock,这时应该设置失败,因为lock这个key已经被线程c0设置成功,此c1,c2获取锁失败;
  3. c1 ,c2(也可能是更多线程) 也同样通过setnx设置key为lock,value为锁失效时间,这时应该设置失败,因为lock这个key已经被线程c0设置成功,此c1,c2获取锁失败;
代码
  public boolean lock(final String key, final String value,Long expireTime) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.expire(serializer.serialize(key), expireTime);
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }

弊端:通过setNX和expire 并不是原子操作,如果在setnx设置成功之后系统崩盘,就会出现死锁

实例二
实现思路
  1. c0通过setnx设置key为lock,value为锁失效时间,可以为当前时间向后10秒,设置成功,成功获取锁;
  2. c1 ,c2(也可能是更多线程) 也同样通过setnx设置key为lock,value为锁失效时间,这时应该设置失败,因为lock这个key已经被线程c0设置成功,此c1,c2获取锁失败;
  3. c1,c2获取lock的过期时间,发现锁过期,进行再次争夺锁;
  4. c1,c2(或者更多线程)发现锁已经过期这个时候每个线程中一定存有lock的value,通过getset 命令设置lock的value,value也是锁的失效时间,如果返回的值和lock之前的value进行对比相同的话,即可认为获取锁成功;

此实例算是实例一的补救;

代码
 /**
     * 加锁
     */
    public  long lock(String lockKey, String threadName) {
        log.info(threadName + "开始执行加锁");
        //锁时间   过期时间
        Long lock_timeout = currtTimeForRedis() + lockTimeout + 1;
        // 锁失效时间为10 秒,value里存入过期时间,如果超过过期时间视为无效锁,其他线程可以重新获取锁
        if (setNX(lockKey, String.valueOf(lock_timeout),10l)) {
            //如果加锁成功
            log.info(threadName + "加锁成功+1");
            //设置超时时间,释放内存
 
            return lock_timeout;
        } else {
            //获取redis里面的时间
            Object result = get(lockKey);
            Long currt_lock_timeout_str = result == null ? null : Long.parseLong(result.toString());
            //锁已经失效
            if (currt_lock_timeout_str != null && currt_lock_timeout_str < currtTimeForRedis()) {
                //判断是否为空,不为空时,如果被其他线程设置了值,则第二个条件判断无法执行
                //获取上一个锁到期时间,并设置现在的锁到期时间
                Long old_lock_timeout_Str = Long.valueOf(getSet(lockKey, String.valueOf(lock_timeout)));
                if (old_lock_timeout_Str != null && old_lock_timeout_Str.equals(currt_lock_timeout_str)) {
                    //多线程运行时,多个线程签好都到了这里,但只有一个线程的设置值和当前值相同,它才有权利获取锁
                    log.info(threadName + "加锁成功+2");
                    //设置超时间,释放内存  这个是 10 L
                    expire(lockKey, 10l);
                    //返回加锁时间
                    return lock_timeout;
                }
            }
        }
        return -1;
    }
  public boolean setNX(final String key, final String value,Long expireTime) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.expire(serializer.serialize(key), expireTime);
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            log.error("setNX redis error, key : {}", key,e);
        }
        return obj != null ? (Boolean) obj : false;
    }

  public String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            log.error("getSet redis error, key : {}", key,e);
        }
        return obj != null ? obj.toString() : null;
    }

  /**
     * 多服务器集群,使用下面的方法,代替System.currentTimeMillis(),获取redis时间,避免多服务的时间不一致问题!!!
     *
     * @return
     */
    public long currtTimeForRedis() {
        Object obj = redisTemplate.execute(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.time();
            }
        });
        return obj == null ? -1 : Long.parseLong(obj.toString());
    }
 public boolean expire(final String lockKey, final Long expireTime) {
        Object obj = null;
        try {
            obj =  redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    boolean bool = connection.expire(serializer.serialize(lockKey), expireTime);
                    connection.close();
                    return bool;
                }
            });
            log.info("设置过期时间"+expireTime);
            return (Boolean)obj;
        } catch (Exception e) {
            log.error("expire redis error, key : {}", lockKey,e);
        }
        return false;
    }

弊端:多线程一起通过getset设置lock的value和之前的value进行比对,最后lock的value并不一定是成功获取锁线程设置的,即成功获取锁的线程的失效时间并不一定是自己想要的失效时间,但是误差极小,而且不影响锁的使用,所以可以忽略;由此设置的锁不具有拥有者的标识,即任何线程都能对此锁释放;

注:在此列举常见的两个实例供大家参考;

redisson引入

贴一段概念:原为地址 https://www.cnblogs.com/liyan492/p/9858548.html

概念:

Jedis:是Redis的Java实现客户端,提供了比较全面的Redis命令的支持,

Redisson:实现了分布式和可扩展的Java数据结构。

Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:

Jedis:比较全面的提供了Redis的操作特性

Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列

Lettuce:主要在一些分布式缓存框架上使用比较多

可伸缩:

Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。

Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作

Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

redisson RLock

​ RLock实现了java.util.concurrent.locks.Lock接口(点击跳转->温习lock接口)。而且RLock是使用Lua脚本完成分布式锁效率更高。

如下:

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

通过anyLockkey获取锁,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

​ 而且也可以通过leaseTime自行控制超时时间

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...//代码逻辑
lock.unlock();

另外redisson还支持公平锁,联锁,红锁,读写锁,信号量,可过期性信号量,闭锁

官网地址:https://redisson.org/

文档地址:https://github.com/redisson/redisson#quick-start

推荐一个不错的参考文章:https://yq.aliyun.com/articles/551423

Zookeeper分布式锁

zookeeper基础

znode 一共4中类型:持久的,临时的,有序持久,有序临时

​ 持久的znode,如/path,只能通过调用delete来进行删除。临时的znode与之相反,当创建该节点的客户端崩溃或关闭了与ZooKeeper的连接时,这个节点就会被删除。

​ 一个znode还可以设置为有序(sequential)节点。一个有序znode节
点被分配唯一个单调递增的整数。当创建有序节点时,一个序号会被追
加到路径之后。例如,如果一个客户端创建了一个有序znode节点,其
路径为/tasks/task-,那么ZooKeeper将会分配一个序号,如1,并将这个
数字追加到路径之后,最后该znode节点为/tasks/task-1。有序znode通过
提供了创建具有唯一名称的znode的简单方式。同时也通过这种方式可
以直观地查看znode的创建顺序。

zookeeper watch机制

​ 当node节点有变化时,为了替换客户端的轮询,我们选择了基于通知(notification)的机制,客户端向ZooKeeper注册需要接收通知的
znode,通过对znode设置监视点(watch)来接收通知。

​ ZooKeeper可以定义不同类型的通知,这依赖于设置监视点对应的
通知类型。客户端可以设置多种监视点,如监控znode的数据变化、监
控znode子节点的变化、监控znode的创建或删除。

看了上面的一些官方的不知道会不会懵逼,简单的说,就是观察者模式,让客户端对node配置监听,如果node有变回,就会有通知回来。

简单测试

  1. 查看zk节点

    [zk: localhost:2181(CONNECTED) 3] ls /
    [zookeeper]
    
  2. 创建/test节点

    [zk: localhost:2181(CONNECTED) 4] create /test ""
    Created /test
    [zk: localhost:2181(CONNECTED) 5] ls /
    [zookeeper, test]
    
  3. 启动客户端2对test节点监听

    [zk: localhost:2181(CONNECTED) 1] ls /
    [zookeeper, test]
    [zk: localhost:2181(CONNECTED) 2] get /test true #true 是打开watch
    
    cZxid = 0x4d
    ctime = Fri Nov 22 21:41:15 CST 2019
    mZxid = 0x4d
    mtime = Fri Nov 22 21:41:15 CST 2019
    pZxid = 0x4d
    cversion = 0
    dataVersion = 0
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 0
    numChildren = 0
    [zk: localhost:2181(CONNECTED) 3]
    
  4. 第一个客户端删除/test节点

    [zk: localhost:2181(CONNECTED) 6] delete /test
    [zk: localhost:2181(CONNECTED) 7]
    
  5. 接着马上客户端2就会有通知

    [zk: localhost:2181(CONNECTED) 3]
    WATCHER::
    
    WatchedEvent state:SyncConnected type:NodeDeleted path:/test
    

zookeeper分布式锁思路

思路一

​ 假设有一个应用由n个进程组成,这些进程尝试获取一个锁。再次
强调,ZooKeeper并未直接暴露原语,因此我们使用ZooKeeper的接口来
管理znode,以此来实现锁。为了获得一个锁,每个进程p尝试创建
znode,名为/lock。如果进程p成功创建了znode,就表示它获得了锁并
可以继续执行其临界区域的代码。不过一个潜在的问题是进程p可能崩
溃,导致这个锁永远无法释放。在这种情况下,没有任何其他进程可以
再次获得这个锁,整个系统可能因死锁而失灵。为了避免这种情况,我
们不得不在创建这个节点时指定/lock为临时节点。

思路二

​ n个进程一起创建顺序节点,如果发现当前线程创建的顺序节点是第一个,则成功获取锁,如果不是则可以鉴定前面的锁释放,如果前面锁释放,则自己成功获取锁。

zookeeper Curator 框架

​ Curator是Netflix公司开源的一套Zookeeper客户端框架。了解过Zookeeper原生API都会清楚其复杂度。Curator帮助我们在其基础上进行封装、实现一些开发细节,包括接连重连、反复注册Watcher和NodeExistsException等。目前已经作为Apache的顶级项目出现,是最流行的Zookeeper客户端之一。从编码风格上来讲,它提供了基于Fluent的编程风格支持。

除此之外,Curator还提供了Zookeeper的各种应用场景:Recipe、共享锁服务、Master选举机制和分布式计数器等。

​ zookeeper 分布式锁通过acquire进行获取锁,通过release进行释放锁

curator框架分布式锁

共享可重入锁
@RestController
public class ZookeeperController {
    @Autowired
    private CuratorFramework curatorFramework;

    @RequestMapping("/zookeeper/makeorder")
    public String makeOrder(Long userId){
        // 生成锁,防止用户频繁点击
        InterProcessMutex lock = new InterProcessMutex  (curatorFramework, "/makeorder"+userId);
        try {
            // 如果可能请尽量设置超时时间,免得一直等待
            lock.acquire();
            System.out.println("获取锁成功");
            //todo
            Thread.sleep(10000L);
            lock.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "true";
    }
    @RequestMapping("/zookeeper/makeorder/trylock")
    public String makeOrderTrylock(Long userId){
        // 生成锁,防止用户频繁点击
        InterProcessMutex lock = new InterProcessMutex  (curatorFramework, "/makeorder"+userId);
        try {
            Boolean getlock = lock.acquire(1000l, TimeUnit.MILLISECONDS);
            if(getlock){
                //todo
                System.out.println("试着获取锁成功");
                lock.release();
                Thread.sleep(4000l);
                return "true";
            }else{
                System.out.println("获取锁失败,获取超时");
                return "false";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }
}

读写锁

    @RequestMapping("/zookeeper/makeorder/readlock")
// 读取锁
    public String makeOrderReadlock(Long userId){
        // 生成锁,防止用户频繁点击
        InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock   (curatorFramework, "/makeorderReadlock"+userId);
        InterProcessMutex lock = interProcessReadWriteLock.readLock();
        try {
            Boolean getlock = lock.acquire(1000l, TimeUnit.MILLISECONDS);
            if(getlock){
                //todo
                System.out.println("试着获取readlock锁成功");
                lock.release();
                return "true";
            }else{
                System.out.println("获取锁失败,获取超时");
                return "false";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }
    @RequestMapping("/zookeeper/makeorder/writelock")
// 写入锁
    public String makeOrderWritelock(Long userId){
        // 生成锁,防止用户频繁点击
        InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock   (curatorFramework, "/makeorderReadlock"+userId);
        try {
            InterProcessMutex lock = interProcessReadWriteLock.writeLock();
            boolean getlock = lock.acquire(2000l, TimeUnit.MILLISECONDS);
            if(getlock){
                //todo
                System.out.println("试着获取writelock锁成功");
                Thread.sleep(10000l);
                lock.release();
                return "true";
            }else{
                System.out.println("获取锁失败,获取超时");
                return "false";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }

curator对分布式锁分装的比较完善,可以很轻松的实现分布式锁,另外还有共享锁不可重入共享信号量联锁队列等等。

官网地址:http://curator.apache.org/curator-recipes/index.html

共享锁一篇源码分析,写的好,供大家参考https://www.cnblogs.com/shileibrave/p/9854921.html

猜你喜欢

转载自blog.csdn.net/qq_30285985/article/details/103227782