一次hashcode引起的血案

 

公司内部做服务优化,线上单机部署多个redis实例,路由到同一台机器上的用户,id号段假设为1000000~9999999,同一个的用户信息肯定是要固定到某个redis实例的,所以需要一个算法,保证每次选择的redis实例都是一样的。最容易想到的就是用id对redis实例个数取余,但如果以后id换为字符串呢?这种取余算法就不合适了。

之后想到可以使用hashcode,把id转换为String,对之取hashcode,然后用hashcode对redis实例个数取余,同样的字符串,计算得到的hashcode肯定是一致的,也就解决了一致性选择redis实例的问题。部分代码如下:

//key为String类型
int hash = 0;
if (StringUtils.isNotBlank(key)) {
    hash = key.hashCode()
}
//根据basePort做偏移,选取不同的redis实例
jedis = getClientFromPool(host, config.basePort + hashcode % redisServerCount);

考虑到字符串可能为空(程序bug或未知原因),默认hashcode为0,根据不同的hashcode,对basePort做偏移初始化的jedis。本以为代码写的很健壮,运行起来肯定万事大吉,直接手工。

但是,事与愿违,服务运行时总是报奇怪的错误如下,究其原因就是,jedis没有初始化,无法从jedis的连接池中获取链接,本机redis服务监听的端口好好的,redis服务本身没有问题,怎么会这样呢?

java.lang.NullPointerException
        at com.xiaomi.xmpush.redis.JedisClient.set(JedisClient.java:490)
        at com.xiaomi.xmpush.redis.JedisClient.set(JedisClient.java:484)


redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
        at redis.clients.util.Pool.getResource(Pool.java:53)
        at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
        at com.xiaomi.xmpush.redis.JedisClient.getClientFromPool(JedisClient.java:69)



那就只可能是代码的问题,无法创建redis实例的链接,那可能是port选的不对?port为什么会不对呢,hashcode取余,保证不会溢出啊!心中很纳闷,怀疑可能是hashcode搞得鬼,果然,增加了一些debug log之后,竟然发现hashcode的值可能为负数。比如这货

long userId = 56800004874L;
System.out.println(String.valueOf(userId).hashCode() );

输出:-2082984168

再看hashcode的源码,返回值是int型,确实可能是负数。。

    /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

大家都知道,int型的值取值范围为Integer.MIN_VALUE(-2147483648)~Integer.MAX_VALUE(2147483647),那怎么修改呢?可以想到取绝对值,那Integer.MIN_VALUE的绝对值是多少呢?正数发生了溢出啊?

Math.abs(Integer.MIN_VALUE) = Integer.MIN_VALUE  

看来这样行不通,如果对hash做数据偏移,统一加2147483647呢?No no,这样更是错误,因为更会发生数据溢出,会产生负数,有一种牛逼的办法就是使用位与操作:

hash = key.hashCode() & Integer.MAX_VALUE; // caution, make value not minus

对于计算得到的hash值,强制把符号位去掉,保证结果只在正数区间,至此,把hashcode关进笼子了,问题解决。

猜你喜欢

转载自blog.csdn.net/chanllenge/article/details/85673259