6.redis分布式算法原理

转载自:https://mp.weixin.qq.com/s?src=11&timestamp=1592644157&ver=2412&signature=pi8qPGJHFwrwWpkJ8b-6UbSHBEc9Sgi0d5RNjgHJmG3sZBunGqvchBwwQA-XffYChgwHNqzKyED0FIVtcBhdKKcs9OTyZYlJ1i6So3q1QB6ev8oR1uPWlLOUl6eSF9aJ&new=1

一.hash算法

散列表(Hash table,也叫哈希表),是依据关键码值(Key value)而直接进行訪问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来訪问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表,如果不使用散列函数,而是直接讲key作为位置进行访问记录,则会浪费很大的空间.所以散列存在是必须的!

散列函数,即哈希算法将任意长度的二进制值映射为固定长度的较小二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。

散列函数用来将key运算成一个地址值,多个地址值组成hash表用来存储value.

整个散列过程其实就是两步。

  (1) 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。

   (2) 当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。由于存取用的是同一个散列函数, 因此结果当然也是相同的。



--------------------------------

  最最简单易懂的说法就是,hash算法将你传入的key运算成一个地址值,类似指针那样,指向内存中的某块区域,存的时候根据该地址值,将value存到这个地址值映射的内存区域里,取得时候从key作hash运算后得出的地址值所对应的内存区域中取出结果;

--------------------------------

  自己的理解:哈希一般是基于数组的,只要知道数组的下表,就可以很快的存取该元素。

  通过对元素的一个关键字,在通过一个函数,把这个关键字转换成数组的下标,这个函数就是哈希函数。

  这样,就能通过关键字很快的存取元素。

  有时候会发生冲突现象,即不同的关键字,通过哈希函数算出的结果是一样的,这时候就可以用线性探测或者连地址法等来解决。

  在使用哈希的时候,要考虑你存的数据个数的大小,然后再确定数组的大小,一般数组的大小是数据个数的两倍(好像)。具体为什么就不是特别清楚了。。。。

--------------------------------

二.hash一致性算法

当我们在做数据库分库分表或者是分布式缓存时,不可避免的都会遇到一个问题:
如何将数据均匀的分散到各个节点中,并且尽量的在加减节点时能使受影响的数据最少。

Hash 取模
随机放置就不说了,会带来很多问题。通常最容易想到的方案就是 hash取模了。

可以将传入的 Key 按照 index=hash(key)%N 这样来计算出需要存放的节点。其中 hash 函数是一个将字符串转换为正整数的哈希映射方法,N 就是节点的数量。

这样可以满足数据的均匀分配,但是这个算法的容错性和扩展性都较差。

比如增加或删除了一个节点时,所有的 Key 都需要重新计算,显然这样成本较高,为此需要一个算法满足分布均匀同时也要有良好的容错性和拓展性。

1. 一致 Hash 算法

一致 Hash 算法是将所有的哈希值构成了一个环,其范围在 0~2^32-1。如下图:

image

之后将各个节点散列到这个环上,可以用节点的 IP、hostname 这样的唯一性字段作为 Key 进行 hash(key),散列之后如下:

object对象hash之后存储在圆环当中成N1,N1=hash(object)

之后需要将数据定位到对应的节点上,使用同样的 hash函数 将 Key 也映射到这个环上。

image

这样按照顺时针方向就可以把 k1 定位到 N1节点,k2 定位到 N3节点,k3 定位到 N2节点

容错性

这时假设 N1 宕机了:

image

依然根据顺时针方向,k2 和 k3 保持不变,只有 k1 被重新映射到了 N3。这样就很好的保证了容错性,当一个节点宕机时只会影响到少少部分的数据。

拓展性

当新增一个节点时:

image

在 N2 和 N3 之间新增了一个节点 N4 ,这时会发现受印象的数据只有 k3,其余数据也是保持不变,所以这样也很好的保证了拓展性。

2.hash倾斜性

到目前为止该算法依然也有点问题:当节点较少时会出现数据分布不均匀的情况:

当hash比较靠近如下图的时候,大多数的数据可能会存储在N1上,N2存储的可能比较少,此时可以使用虚拟节点来解决这种问题。

虚拟节点
hash倾斜性

这样会导致大部分数据都在 N1 节点,只有少量的数据在 N2 节点。

为了解决这个问题,一致哈希算法引入了虚拟节点。将每一个节点都进行多次 hash,生成多个节点放置在环上称为虚拟节点:

虚拟节点

计算时可以在 IP 后加上编号来生成哈希值。

这样只需要在原有的基础上多一步由虚拟节点映射到实际节点的步骤即可让少量节点也能满足均匀性。

三.redis分布式缓存

redis分布式缓存池是通过ShardedJedisPool来实现的,在本项目当中使用的配置和3redis连接池当中一样,在本例当中使用了2个redis节点,所以,创建了2个

package com.mall.common;

import com.mall.util.PropertiesUtil;
import redis.clients.jedis.*;
import redis.clients.jedis.util.Hashing;
import redis.clients.jedis.util.Sharded;

import java.util.ArrayList;
import java.util.List;

//redis分片连接池
public class RedisShardedPool {

    //ShardedJedisPool的连接池

    private static ShardedJedisPool pool;
    //最大连接数
    private static Integer maxTotal=Integer.parseInt(PropertiesUtil.getProperty("redis.max.total","20"));
    //最大空闲的连接数
    private static Integer maxIdle=Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle","10"));
    //最小空闲连接数
    private static Integer minIdle=Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle","2"));
    //从jedis连接池获取连接时,校验并返回可用的连接
    private static Boolean testOnBorrow=Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow","true"));
    //把连接放回jedis连接池时,校验并返回可用的连接
    private static Boolean testOnReturn=Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return","true"));

    /**
     * 此处由于配置了2个redis节点,所以此处要配置2个
     */
    private static String redisIp1=PropertiesUtil.getProperty("redis1.ip");
    private static Integer redisPort1=Integer.parseInt(PropertiesUtil.getProperty("redis1.port"));
    private static String redisIp2=PropertiesUtil.getProperty("redis2.ip");
    private static Integer redisPort2=Integer.parseInt(PropertiesUtil.getProperty("redis2.port"));

    private static void initPool(){
        JedisPoolConfig config=new JedisPoolConfig();
        config.setMaxTotal(maxTotal);//默认8
        config.setMaxIdle(maxIdle);//默认8
        config.setMinIdle(minIdle);//默认0

        config.setTestOnBorrow(testOnBorrow);//默认false
        config.setTestOnReturn(testOnReturn);//默认false

        //默认就是true,连接耗尽的时候是否阻塞,false会报异常,true阻塞直到超时,超时会报超时异常
        config.setBlockWhenExhausted(true);

        JedisShardInfo info1=new JedisShardInfo(redisIp1,redisPort1,1000*2);
        JedisShardInfo info2=new JedisShardInfo(redisIp2,redisPort2,1000*2);
        //此处是两个redis节点,不指定会默认arraylist的size
        List<JedisShardInfo> jedisShardInfoList= new ArrayList<>(2);
        jedisShardInfoList.add(info1);
        jedisShardInfoList.add(info2);

        /**
         * redis会提供2个分片策略:一个是MURMUR_HASH(即一致性hash算法),一个是md5的hash算法(普通的hash)
         */
        pool=new ShardedJedisPool(config,jedisShardInfoList, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN);

    }

    //类在加载到jvm的时候加载连接池
    static {
        initPool();
    }

    //获取redis
    public static ShardedJedis getJedis(){
        return pool.getResource();
    }

    //将redis放回连接池,释放资源
    public static void close(ShardedJedis shardedJedis){
        if(shardedJedis !=null){
            shardedJedis.close();
        }
    }


    public static void main(String[] args) {
        ShardedJedis shardedJedis=pool.getResource();// Jedis jedis=RedisPool.getJedis()
        /*shardedJedis.set("name","codeSheep");
        close(shardedJedis);*/

        //pool.destroy();//销毁连接池当中的所有连接

        for (int i=0;i<10;i++){
            shardedJedis.set("key"+i,"value"+i);
        }
        close(shardedJedis);
        System.out.println("program is end");
    }

}

效果
package com.mall.util;


import com.mall.common.RedisShardedPool;
import com.mall.common.RedisShardedPool;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ShardedJedis;

@Slf4j
public class RedisShardedPoolUtil {

    public static String set(String key,String value){
        ShardedJedis redisShardedPool= RedisShardedPool.getJedis();//拿到一个redis的连接
        String result=redisShardedPool.set(key,value);
        RedisShardedPool.close(redisShardedPool);
        return result;
    }

    public static String get(String key){
        ShardedJedis shardedJedis=RedisShardedPool.getJedis();
        String value=shardedJedis.get(key);
        RedisShardedPool.close(shardedJedis);
        return value;
    }

    //exTime的单位是s,设置key的值为value,存活时间是exTime
    public static String setEx(String key,String value,int exTime){
        ShardedJedis shardedJedis=RedisShardedPool.getJedis();//拿到一个redis的连接
        //设置key的值为value,存活时间是exTime
        String result=shardedJedis.setex(key,exTime,value);
        RedisShardedPool.close(shardedJedis);
        return result;
    }

    //重置key的有效期
    public static Long expire(String key,int exTime){
        ShardedJedis shardedJedis=RedisShardedPool.getJedis();//拿到一个redis的连接
        //设置key的值为value,存活时间是exTime
        Long result=shardedJedis.expire(key,exTime);
        RedisShardedPool.close(shardedJedis);
        return result;
    }

    //删除key
    public static Long del(String key){
        ShardedJedis shardedJedis=RedisShardedPool.getJedis();//拿到一个redis的连接
        //删除key
        Long result=shardedJedis.del(key);
        RedisShardedPool.close(shardedJedis);
        return result;
    }

    public static void main(String[] args) {
        String result= RedisShardedPoolUtil.set("sex","男");
        String value= RedisShardedPoolUtil.get("sex");
        System.out.println(value);

        RedisShardedPoolUtil.setEx("zhandouli","10000",60*10);

        RedisShardedPoolUtil.expire("sex",60*20);
        //RedisShardedPoolUtil.del("sex");
        System.out.println("end");

    }


}