jdk8.github.io

1. 缓存

1.1 缓存一致性问题

一段关于好友缓存的代码,伪代码如下:

1.LRUCache friend_list_cache = LRUCache(1000);

// 更新
2.void add_friend(string my_id, string add_id) {
3.	update_friend_list_cache(my_id, add_id);
4.	update_db(my_id, add_id);
5.}

// 读取
6.list<string> get_friend(string my_id) {
7.	if (!cache.contain(my_id)) {
8.		list<string> friend_list = read_db(my_id);
9.		insert_friend_list_cache(my_list, friend_list);
10.		return friend_list;
11.	} else {
12.		return cache.get(my_id);
13.	}
14.}

业界常用的方法Cache Aside Pattern:

15.void add_friend(string my_id, string add_id) {
16.	update_db(my_id, add_id);
17.	delete_friend_list_cache(my_id);
18.}

总结一下主要是三种:

1.2 缓存问题

1.2.1 缓存穿透

非法请求一个不存在的key,绕过缓存直接请求mysql,解决办法:

1.2.2 缓存击穿

同时大量请求某个合法的key,且不存在缓存中,则大量请求打到mysql,解决版本:

1.2.3 缓存雪崩

redis失效,大量请求到mysql,解决办法:

1.2.4 大量热点key失效

key设置不同的失效时间。

2. Redis

2.1 常用命令

事务

List列表

无序集合

有序集合

2.2 基于Redis实现分布式锁

在分布式环境下,需要借助第三方工具来实现锁,Redis官方提供了一种基于Redis实现的分布式锁redlock

单节点版本

try_lock返回true,表示成功获取锁,返回false,表示获取锁失败。lock_id为锁的ID,unique_id为每次请求锁的唯一的标识,可以用UUID,lock_valid_time为锁的有效时间,过期后,锁将会自动释放。

release_lock为释放锁的方法,释放锁要带上与single_try_lock相同的lock_id和unique_id。

bool  try_lock(string lock_id, string unique_id, int leaseTime) {
    // setnx - SET if Not eXists
    return redis.setnx_expire(lock_id, unique_id, leaseTime);
}

void release_lock(string lock_id, string unique_id) {
    if (redis.get(lock_id) == unique_id) { // 避免某个客户端释放另外一个客户端的锁
        redis.del(lock_id);
    }
}

这里存在一个问题:某个成功获取锁的客户端A认为自己获得到了锁,此时由于网络延时/CPU抢占/GC,距离请求锁时已经过了很长时间,如果这个时间超过了锁的有效时间,那么锁就可能自动释放,线程B获取锁,此时A还认为自己持有锁,对临界区进行修改。

可以稍微对代码进行修改,解决这个问题,代码如下,try_lock_v2返回的是Lock对象,对临界区进行操作前,需要调用Lock的is_lock_valid判断锁的有效性。

struct Lock {
    int leaseTime;
    int begin_lock_time;
    bool is_get_lock;
    bool is_lock_valid() {
        int current_time = get_current_timestamp_millisecond();
        return is_get_lock && (current_time - begin_lock_time < lock_valid_time);
    };
};

Lock try_lock_v2(string lock_id, string unique_id, int leaseTime) {
    Lock lock;
    lock.leaseTime = leaseTime;
    lock.begin_lock_time = get_current_timestamp_millisecond()
    lock.is_get_lock = redis.setnx_expire(lock_id, unique_id, leaseTime);
    return lock;
}

集群版本

Redis集群保证了锁服务的可用性,try_lock返回的是Lock对象,可以通过Lock的is_lock_valid判断是否独占了锁。算法的核心思想是:从超过半数的节点获得锁,就认为获取锁成功。有几点需要注意:

Lock try_lock(string lock_id, string unique_id, int leaseTime, int clock_drift) {
    Lock lock;
    int get_lock_count = 0;
    for (auto server : servers) {
        if (server.try_lock(lock_id, unique_id, leaseTime)) {
            get_lock_count += 1;
        }
    }
    int elapsed_time = get_current_timestamp_millisecond() - lock.begin_lock_time;
    lock.leaseTime = leaseTime - elapsed_time - clock_drift;
    lock.lock_begin_time = get_current_timestamp_millisecond()
    if (lock.leaseTime > 0 && get_lock_count > servers.size()/2) {
        lock.is_get_lock = true;
    } else {
        lock.is_get_lock = false;
    }
    return lock;
}

void release_lock(string lock_id, string unique_id) {
    for (auto server : servers) {
        server.release_lock(lock_id, unique_id);
    }
}

try_lock如何调用到指定server? 一种做法是在lock_id上制定hash tag。从master查到slot信息,比如每个master管理什么slot。 指定key的副本数replica,比如为3。根据slot反查出一个key,将key作为hash tag放到lock_id上。这样这个lock_id就可以指定调用到某个master。

2.3 redis实现信号量

public class Semaphore_Lock {
    public static String acquire_semaphore(Jedis redis,String sem_name,int limit,long timeout){
        String identifier=UUID.randomUUID().toString();
        long now=System.currentTimeMillis();
        Pipeline pipeline=redis.pipelined();
        //清理其他持有者过期信号量
        pipeline.zremrangeByScore(sem_name, 0, now-timeout);
        pipeline.zadd(sem_name,now,identifier);
        Response<Long> rank=pipeline.zrank(sem_name, identifier);
        pipeline.syncAndReturnAll();
        if((Long)rank.get()<limit){
            System.out.println(Thread.currentThread().getName()+"  identifier rank :"+rank.get());
            return identifier;
        }else{
            System.out.println(Thread.currentThread().getName()+"  identifier rank :"+rank.get()+",too late");
        }
        redis.zrem(sem_name, identifier);
        return null;
    }
    public static Long release_semaphore(Jedis redis,String sem_name,String identifier){
        return redis.zrem(sem_name, identifier);
    }
}

3. Redis Cluster

3.1结构

Redis Cluster保证了可用性,不保证强一致性。Redis Cluster为N主N从,主从复制的结构。从节点不保证数据和主节点一致,如果有一致性的需求,可以禁用掉从读。

3.2 Sharding

Hash slot

Redis Cluster将key空间分为16384个槽位(slot),又称为哈希槽(hash slot),每个master节点处理一个hash slot子集,hash slot的计算方式如下:

def HASH_SLOT(key):
    return CRC16(key) % 16384

如果希望两个不同的key映射到同一个slot,可以用hash tag,带hash tag的slot计算方式如下:

def HASH_SLOT(key):
    if "{" in key and "}" in key:
        s = key.index("{") + 1
        e = key.index("}")
        if e > s:
            return CRC16(key[s:e]) % 16384
    return CRC16(key) % 16384

Hash slot VS Consistant hashing

一致性哈希在变更节点时,需要遍历节点的所有key,判断key是否需要迁移。Hash slot在变更节点时,只需要遍历slot,判断slot是否迁移。

3.3 数据迁移

当cluster新增master或者下线master时,涉及到slot的重新分配,即从一个master迁移到另一个master。如果需要将slot X从master A迁移到master B,需要做如下操作:

master B执行:CLUSTER SETSLOT X IMPORTING A # 设置B slot X为从A迁入状态
master A执行:CLUSTER SETSLOT X MIGRATING B # 设置A slot X为迁出到B状态
master A执行:MIGRATE HOST_B PORT_B "" 0 5000 KEYS key1 key3 # 迁移数据,可以选择迁移单个key或者多个key。键迁移期间,A和B处于阻塞状态,保证一致性。
master B执行:CLUSTER SETSLOT X NODE B # 设置X为B的slot

下图为迁移过程客户端访问slot X的结果,基于hash slot的寻址方式需要客户端记录slot和master节点的映射关系。如果某个slot不在当前访问的master节点,就会返回MOVE,客户端缓存slot和master节点的信息。迁移时,旧key仍需要在源节点处理,新key需要到新节点处理,所以需要增加ASK状态,表示仅在下次访问新key时,转到新节点,并不更新客户端缓存,如果缓存被更新,旧key也被路由到新节点了,显然是错的。

在这里插入图片描述

3.4 Gossip协议

Redis Cluster使用了gossip协议。gossip协议中,每个节点A执行如下步骤:

Step1:选择K个相邻节点,对每个选择的节点B,A与B其交换信息
Step2:等待T秒
Step3:执行Step1

上述算法中有两处决定了gossip协议的性能:

gossip协议最大的优点是去中心,消息可靠投递。缺点是消息传递到所有节点需要 O(logN)* T 秒,N是节点数量,且消息有冗余。