关于redis探究2-----分布式部署,io通信模型,分布式锁等内容的学习与研究

前言

在之前我有写过关于redis相关数据结构的学习文章,本文接着上文的内容,来研究一些redis一些其他特性方面的内容,从而更加全面的掌握与了解redis的使用。
本文大概会根据个人使用的心得去总结和探讨关于redis的io通信模型,redis相关持久化以及redis在分布式部署情况下的一些注意事项和相关方法与原理。最后再来学习一下redis作为分布式锁等使用场景。文章内容可能稍显杂乱。

关于redis的io模型

关于redis客户端的建立

在介绍redis的io模型之前,我们首先了解一下redis如何与客户端建立连接.redis通过在tcp端口上进行监听,或者使用socket的方式来接收客户端的连接。当一个新的客户端连接请求被接受后,将执行以下操作:
1. 当redis使用非阻塞i/o复用,客户端socket将被设置为非阻塞状态
2. socket TCP_NODELAY属性将会被设置,确保连接中不会出现延迟。
3. 一个可读的文件事件被创建,因而新的数据可以被访问,redis可以更快的接受客户端的查询。
在上述操作完成后,redis完成对客户端的初始化后进行客户端连接数量检查,保证连接数量不会超过最大连接数。

redis的io模型

我们在平时使用redis过程中或多或少的听说过redis是单进程单线程的应用,也听说过redis具有较高的io效率,那么redis究竟是怎么做到这两个几乎矛盾的事情呢? 我们首先回顾一下关于io方面的一些基本知识,io的过程一般分为两步,第一阶段是数据准备阶段,第二阶段是数据复制阶段,在复制阶段就是将数据从内核态复制到用户空间中。通常我们提到io时也经常会提到阻塞io,非阻塞io,同步io,异步io,以及多路复用等概念。所谓阻塞io即为当一个客户端发送请求之后,该线程会被挂起,直到服务端返回结果后才会进行后续的逻辑处理。而非阻塞io则是在客户端发送请求挂起后定时的检查服务端数据的准备情况,但是当数据返回后,非阻塞io也会像非阻塞io一样通过阻塞的方式将数据完成复制。而同步io和异步io则是根据io在进行中是否阻塞进行描述,如果在io过程中会发生阻塞情况则这种io属于同步io,如果不会发生阻塞则属于异步io。而redis则是使用了非阻塞的io形式,从而具有了较好的io处理效率。而另外一点是redis使用了多路复用技术,所谓多路复用技术即服务端使用轮询的技术通过同一个通信通道处理不同的客户端请求。在redis中,redis使用多路复用api同时监听多个客户端的读写操作,这些api往往会有相关的timeout,在这个时间内redis线程会阻塞,进而进行套接字的监听,在这个过程中redis会给每个客户端套接字匹配一个指令队列,按照指令队列进行处理,同时将操作结果放到输出队列。

REDIS 分布式锁

依照惯例,我们在介绍主题之前首先来介绍下关于分布式的一些基础概念,首先介绍下什么是分布式。所谓分布式系统是通过网络进行通信,从而将一组计算机可以协调起来共同完成任务,其目的是为了使用多台计算机完成一台1计算机无法去完成的任务。关于分布式系统,我们经常提到CAP理论。那么所谓的CAP理论又是什么呢?其实cap定理描述了分布式系统的相关特性,即在一个分布式系统中无法同时满足一致性,分区容错性,和一致性。通常我们我我们在开发系统时,经常优先保证系统的容错性和可用性。放弃对一致性的保证,但是这并不代表不需要一致性,只是我们通过相关的技术方式,保证最终结果可以一致,这种特性一般被称为最终一致性。
那么我们要谈到的分布式锁和上述理论有什么联系吗?我们都知道在编程开发中为了保证数据的一致性我们通常采用加锁的方式保证在一段时间内可以以独占的形式去修改数据。而分布式锁也是基于同样的目的去实现,利用分布式锁,我们可以保证在一段时间内只有一个进程可以去修改数据,从而保证了数据的一致性。

如何实现

上面我们讲了关于分布式锁的产生原因和背景,我们现在来看看通过redis如何去实现一个分布式锁。我们在设计锁之前,首先要去考虑一个问题,即这个锁究竟应该保证哪些特性?对于这个问题我们可以去参考例如java语言中锁的实现,我们可以得到以下几点:

  1. 对于锁首先要保证得到锁的进程可以独占的方式去访问被锁保护的资源,在命令执行完成后,锁可以被释放。
  2. 对于同一个进程需要保证对于锁的可重入性
  3. 锁应该具有较高的获取和释放性能。
  4. 不会产生死锁的情况。
    接下来我们来看一究竟如何去实现一个分布式锁呢,我们可以这样去设计:
    ① 我们在获取锁时通过redis的SETNX命令去设置key,如果key不存在则创建key,并对key进行赋值,为了保证锁的唯一性,我们使用随机生成的uuid,当键值对创建完成后返回1,如果key存在则返回。
    ② 再锁创建完成后,我们再去给锁设置一个超时时间,当锁到期后,自动完成释放。
    ③ 在获取锁的操作时,我们需要给获取操作加上超时时间,当获取锁操作在时间内未获得锁时,放弃获取锁的操作。
    ④在释放锁时,通过之前锁的uuid对锁进行释放,若是该锁使用del命令将锁删除。

实现代码

//锁相关代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

public class DistributeLock {
    private final JedisPool jedisPool;

    public DistributeLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }
/*
//加锁代码,通过向redis设置以锁名为key和uuid为value的键值对,如果设置成功,
//则证明该客户端获得锁继续执行后面逻辑,如果失败调用sleep方法将线程暂时挂起,等待获得锁的线程执行完毕。
/lockName 锁的名称亦即锁的key。acquireTimeout获取锁的超时时间。timeout锁的超时时间
 */

    public String lockWithTimeOut(String lockName, long acquireTimeOut, long timeout) {
        Jedis conn = null;
        String resIdentifier = null;
        try {
            conn = jedisPool.getResource();
            String identifier = UUID.randomUUID().toString();
            String lockKey = "lock:" + lockName;
            int lockExpire = (int) (timeout / 1000);

            long end = System.currentTimeMillis() + acquireTimeOut;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    resIdentifier = identifier;
                    return resIdentifier;
                }
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return resIdentifier;
    }
//释放锁的代码,使用redis的watch方法去监视key,若key被修改则放弃后续事务执行,否则利用事务的方式,将锁释放。
    public boolean releaseLock(String lockName, String indentifier) {
        Jedis conn = null;
        String lockKey = "lock" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                conn.watch(lockKey);
                if (conn.get(lockKey) != null) {
                    if (indentifier.equals(conn.get(lockKey))){
                        Transaction transaction = conn.multi();
                        transaction.del(lockKey);
                        List<Object> results = transaction.exec();
                        if (results == null) {
                            continue;
                        }
                        retFlag = true;
                    }
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}

public class Service {
    private static JedisPool pool = null;
    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(200);
        config.setMaxIdle(8);
        config.setMaxWaitMillis(1000 * 100);
        config.setTestOnBorrow(false);
        pool = new JedisPool(config, "127.0.0.1",6379, 3000);
    }

    DistributeLock lock = new DistributeLock(pool);
    AtomicInteger n = new AtomicInteger(50);

    public void kill() {
        String identifier = lock.lockWithTimeOut("killLock", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "获得了锁");
        System.out.println(n.decrementAndGet());
        lock.releaseLock("killLock", identifier);
    }
}
public class TestThread implements Runnable {
    private Service service;
    public TestThread(Service service) {
        this.service = service;
    }
    @Override
    public void run() {
        service.kill();
    }
}
public class Test {
    public static void main(String [] args) {
        Service service = new Service();
        for (int i = 0; i < 50; i++) {
            TestThread thread = new TestThread(service);
            Thread t = new Thread(thread);
            t.start();
        }
    }
}

执行结果如下,可见每个线程已经成功的获取了锁并进行执行。
在这里插入代码片

redis分区

所谓reids分区就是将不同的数据分发到不同的redis实例的过程,每个redis实例只具有所有key的一部分。通过分区redis可以去管理更大的内存,否则redis只能使用一台机器的内存。其二通过分区使得redis的计算能力可以显著增强,也会增大redis的带宽。

①reids分区方法

例如我们要在多个redis实例中去存储user:1到user:n,此时我们有如下几种分区方案:
1. 最简答的方法就是根据用户id的范围进行分区,每个redis实例中存储用户数据的一部分,例如第一用户存储1-1000个用户的数据,另一台实例存储1001-2000的数据,使用这种方法我们可以快速的将数据分别存储到不同的实例中,但是这个方法也有一个显而易见的缺点,我们需要去构建一张表来描述数据到redis的映射关系。对于这张表的维护往往需要很大的开销,这就大大增加了这种分区方式的使用复杂度。
2. 另一种分区方式是通过散列进行分区,我们使用hash函数将键名转化为数字,将键转化为数字后我们对生成的散列值进行取模。用来产生对应redis实例个的数字。通过生成的数字将数据存储到对应的redis实例中
3. 一致性哈希,一致性哈希算法算是一种高级的散列分区方式,首先我们需要去构造一个0到2的32次方的整数环,根据缓存服务器的名称或者ip值计算出服务器的hash值,根据计算的hash值将服务器放在hash环上,每次根据要缓存的key值计算key的hash值,在hash环上顺时针找到距离最近的缓存服务器节点。
4. 虽然hash一致性算法已经可以很好的实现redis对数据的负载均衡,但是对于redis的集群确是用另一种方式去实现。这种方式被称为数据分片。在一个redis集群中包含16384个哈希槽,每个key都属于这16384个哈希槽中的一个,集群使用crc(key)% 16384来计算数据的key属于哪个槽。redis集群中的每个节点负责一定数量的槽。

②不同分区方案的实现

redis的分区可以在程序的不同层次进行:
①客户端分区:就是在客户单已经决定数据会被存储到哪个redis节点或者数据将被哪个redis节点读取。
②代理分区:客户端将请求发送给代理,然后代理去决定从哪个节点去存数据或者读数据。
③查询路由:客户端随机的请求任意一个redis实例,然后由这个redis实例将请求转发给正确的redis节点。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值