redis和zookeeper分布式锁实现及比对

本文深入解析Redis和ZooKeeper两种分布式锁的实现原理与应用场景。Redis分布式锁采用RedLock算法,确保互斥、防死锁及高可用性;ZooKeeper分布式锁利用临时节点特性,实现简洁且低开销的锁机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、redis分布式锁

官方支持的一种分布式锁,叫做RedLock算法。

这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点或者这个锁就可以加可以释放)!

1、最普通的实现方式

就是在redis里创建一个key算加锁    SET my:lock 随机值 NX PX 30000,这个命令就ok。

这个的NX的意思就是只有key不存在的时候才会设置成功,PX 30000的意思是30秒后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。

释放锁就是删除key,但是一般可以用lua脚本删除,判断value一样才删除:


if redis.call("get",KEYS[1]) == ARGV[1] then

return redis.call("del",KEYS[1])

else

    return 0

end

为啥要用随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除key的话会有问题,所以得用随机值加上面的lua脚本来释放锁。

但是这样是肯定不行的。因为如果是普通的redis单实例,那就是单点故障。或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。

2、RedLock算法

这个场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁:

1)获取当前时间戳,单位是毫秒

2)跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒

3)尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)

4)客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了

5)要是锁建立失败了,那么就依次删除这个锁

6)只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

二、ZooKeeper分布式锁

对于两个系统同时对一个资源进行处理的时候,通过zk其一系统会去zk创建一个临时节点,创建成功就表示获取到锁了,当其他系统过来创建锁就会失败,只能注册监听去监听这个锁,当删除这个节点的时候也就是释放锁的时候就会通过监听通知给等待的系统去重新加锁。

多客户端竞争锁时代码实现:

/**
* 简单设计的zk分布式锁
*/

public class ZooKeeperDistributedLock implements Watcher{
	
    private ZooKeeper zk;
    private String locksRoot= "/zklocks";
    private String productId;
//这两个节点不应该定义在这的,简写!
    private String waitNode;
    private String lockNode;
//不严谨,这里不应该直接在一个里面进行进程的阻塞,简写!
    private CountDownLatch latch;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000; 

    public ZooKeeperDistributedLock(String productId){
        this.productId = productId;
         try {
	   String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
            zk = new ZooKeeper(address, sessionTimeout, this);
            connectedLatch.await();
        } catch (IOException e) {
            throw new LockException(e);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }

    public void process(WatchedEvent event) {
        if(event.getState()==KeeperState.SyncConnected){
            connectedLatch.countDown();
            return;
        }

        if(this.latch != null) {  
            this.latch.countDown(); 
        }
    }

    public void acquireDistributedLock() {   
        try {
            if(this.tryLock()){
                return;
            }
            else{
                waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        } 
}

/*
* 对节点进行加锁
*/
    public boolean tryLock() {
        try {
 		// 传入进去的locksRoot + “/” + productId
		// 假设productId代表了一个商品id,比如说1
		// locksRoot = lockszk
		// /locks/10000000000,/locks/10000000001,/locks/10000000002
            lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
   
            // 看看刚创建的节点是不是最小的节点
	 	// locks:10000000000,10000000001,10000000002
            List<String> locks = zk.getChildren(locksRoot, false);
            Collections.sort(locks);
	
            if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
                //如果是最小的节点,则表示取得锁
                return true;
            }
	
            //如果不是最小的节点,找到比自己小1的节点
	  int previousLockIndex = -1;
            for(int i = 0; i < locks.size(); i++) {
		if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
	         	    previousLockIndex = i - 1;
		    break;
		}
	   }
	   
	   this.waitNode = locks.get(previousLockIndex);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }
     
    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
        if(stat != null){
            this.latch = new CountDownLatch(1);
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);            	   this.latch = null;
        }
        return true;
}


/*
* 对节点进行释放锁
*/
    public void unlock() {
        try {
		// 删除/locks/10000000000节点
		// 删除/locks/10000000001节点
            System.out.println("unlock " + lockNode);
            zk.delete(lockNode,-1);
            lockNode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
}

    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        public LockException(String e){
            super(e);
        }
        public LockException(Exception e){
            super(e);
        }
}

通过封装库进行操作:

添加maven依赖:

<dependencies>
		<dependency>
			<groupId>com.101tec</groupId>
			<artifactId>zkclient</artifactId>
			<version>0.10</version>
		</dependency>
	</dependencies>

创建Lock锁接口:

public interface Lock {
    //获取到锁的资源
	public void getLock();
    // 释放锁
	public void unLock();
}

创建ZKAbstractLock抽象类

//将重复代码写入子类中..
public abstract class ZookeeperAbstractLock implements Lock {
	// zk连接地址
	private static final String CONNECTSTRING = "127.0.0.1:2181";
	// 创建zk连接
	protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
	protected static final String PATH = "/lock";

	public void getLock() {
		if (tryLock()) {
			System.out.println("##获取lock锁的资源####");
		} else {
			// 等待
			waitLock();
			// 重新获取锁资源
			getLock();
		}

	}

	// 获取锁资源
	abstract boolean tryLock();

	// 等待
	abstract void waitLock();

	public void unLock() {
		if (zkClient != null) {
			zkClient.close();
			System.out.println("释放锁资源...");
		}
	}

}

创建:zkDistrbuteLock类

public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
	private CountDownLatch countDownLatch = null;

	@Override
	boolean tryLock() {
		try {
			zkClient.createEphemeral(PATH);
			return true;
		} catch (Exception e) {
//			e.printStackTrace();
			return false;
		}

	}

	@Override
	void waitLock() {
		IZkDataListener izkDataListener = new IZkDataListener() {

			public void handleDataDeleted(String path) throws Exception {
				// 唤醒被等待的线程
				if (countDownLatch != null) {
					countDownLatch.countDown();
				}
			}
			public void handleDataChange(String path, Object data) throws Exception {

			}
		};
		// 注册事件
		zkClient.subscribeDataChanges(PATH, izkDataListener);
		if (zkClient.exists(PATH)) {
			countDownLatch = new CountDownLatch(1);
			try {
				countDownLatch.await();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		// 删除监听
		zkClient.unsubscribeDataChanges(PATH, izkDataListener);
	}

}

使用zk锁运行效果:

public class OrderService implements Runnable {
	private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
	// 使用lock锁
	// private java.util.concurrent.locks.Lock lock = new ReentrantLock();
	private Lock lock = new ZookeeperDistrbuteLock();
	public void run() {
		getNumber();
	}
	public void getNumber() {
		try {
			lock.getLock();
			String number = orderNumGenerator.getNumber();
			System.out.println(Thread.currentThread().getName() + ",生成订单ID:" + number);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unLock();
		}
	}
	public static void main(String[] args) {
		System.out.println("####生成唯一订单号###");
//		OrderService orderService = new OrderService();
		for (int i = 0; i < 100; i++) {
			new Thread( new OrderService()).start();
		}
	}
}

三、redis和zk分布式的对比

①redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能

②zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小

③另外一点就是,如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁

④redis分布式锁比较麻烦,遍历上锁,计算时间等等。。。zk的分布式锁语义清晰实现简单

<think>我们正在讨论如何通过灰度发布策略将存储层从Redis实现迁移到Ignite实现,同时保持系统稳定。根据之前的设计,我们有两个存储层的实现:ClientImageCacheMemImpl(基于RedisClientImageCacheRedissonImpl(基于Ignite?但注意之前提到的是Ignite在Repository层,而Cache层是MemRedisson。但根据用户当前的问题,是在存储层将Redis迁移到Ignite,所以我们这里需要重新审视一下)。实际上,用户描述中,ClientImageCache接口有两个实现:Mem(内存)Redisson(基于Redis的分布式缓存),而Repository层中有一个Ignite实现(ClientImageRepositoryIgniteImpl)一个依赖Cache的CacheImpl(ClientImageRepositoryCacheImpl,它依赖ClientImageCache,因此可以使用Redis或Mem实现)。现在用户想要迁移的是存储层,将Redis(即ClientImageCacheRedissonImpl)迁移到Ignite(即ClientImageRepositoryIgniteImpl)。但这里注意,Ignite是Repository层的实现,而Redis是Cache层的实现,它们是两个不同的层次。所以我们需要明确:用户是否想要将原本使用Redis作为缓存的存储层,替换成使用Ignite作为存储层的实现?如果是这样,那么实际上是两个不同的接口的实现(Cache接口Repository接口),因此迁移可能涉及从使用Cache层(Redis)切换到使用Repository层(Ignite实现)。因此,我们可能需要分步骤:1.首先,系统原本可能使用的是ClientImageRepositoryCacheImpl(它内部依赖ClientImageCache接口,而该接口的实现可能是Redis,即ClientImageCacheRedissonImpl)。2.现在,我们想要使用ClientImageRepositoryIgniteImpl(它直接使用Ignite,不经过Cache层)来替换。所以,迁移实际上是替换Repository层的实现:从CacheImpl(内部用Redis)迁移到IgniteImpl(直接使用Ignite)。灰度发布策略如下:步骤1:并行部署新实现-在系统中同时保留两种实现:ClientImageRepositoryCacheImpl(使用RedisClientImageRepositoryIgniteImpl(使用Ignite)。步骤2:引入流量路由机制-通过配置中心(如SpringCloudConfig,Zookeeper等)动态控制每个请求使用哪种实现。-在Repository的接口上,我们可以设计一个代理类,根据灰度策略(例如按用户ID、请求比例等)决定将请求转发到旧实现Redis)还是新实现(Ignite)。步骤3:定义灰度规则-初始阶段,设置小比例(如1%)的流量到Ignite实现,其余99%的流量继续使用Redis实现。-规则可以基于:1.用户ID取模(例如用户ID除以100的余数小于1,则使用Ignite)2.请求的特定标记(如HTTPHeader中携带的特定字段)3.随机百分比步骤4:监控与观察-在灰度过程中,密切监控系统指标:1.错误率(特别是Ignite实现的错误率)2.性能指标(响应时间,吞吐量)3.资源使用情况(CPU,内存,网络)4.数据一致性(通过比对日志确保两种实现的结果一致)步骤5:逐步扩大灰度范围-如果监控指标正常,逐步增加灰度比例(5%,10%,50%...)直到100%。步骤6:处理问题与回滚-如果在灰度过程中发现问题,立即减小灰度比例或回滚到全量Redis实现。步骤7:完成迁移-当100%流量切换到Ignite实现且稳定运行一段时间后,移除旧的Redis实现代码相关配置。技术实现示例(Java伪代码):首先,定义一个代理类,实现ClientImageRepository接口,并根据路由规则选择具体的实现:```javapublicclassClientImageRepositoryRouterimplementsClientImageRepository{privateClientImageRepositoryredisRepository;//使用Redis的旧实现privateClientImageRepositoryigniteRepository;//使用Ignite的新实现//灰度比例,从配置中心动态获取privatedoublegrayRatio=0.01;//通过配置中心更新灰度比例publicvoidsetGrayRatio(doublegrayRatio){this.grayRatio=grayRatio;}@OverridepublicImagegetImage(StringimageId){if(shouldUseIgnite()){returnigniteRepository.getImage(imageId);}else{returnredisRepository.getImage(imageId);}}//灰度规则:这里使用随机数,也可以根据业务规则(如用户ID)privatebooleanshouldUseIgnite(){returnMath.random()< grayRatio;}}```步骤8:数据迁移-如果IgniteRedis存储的数据结构不同,我们可能需要将数据从Redis迁移到Ignite。-可以在灰度过程中双写:即在写入Redis的同时也写入Ignite(但这样要注意事务问题,可能导致数据不一致)。-另一种方式是灰度前全量迁移,但这样需要停机。所以更合适的做法是:1.全量迁移历史数据(使用ETL工具)2.在灰度期间,对于还在使用Redis的读操作,数据可能是旧的(因为新写入的数据只写入了Ignite?),所以需要同步写两个存储(双写)直到灰度完成。或者通过代理层,在读操作时如果发现Ignite中没有,则从Redis读取并回写到Ignite(类似缓存回填)。3.在写操作时,同时写入RedisIgnite(双写)。但注意,双写可能会影响性能,且需要保证事务。如果无法保证事务,那么需要确保最终一致性。如果旧的数据量很大,且允许在迁移过程中停机,可以在夜间停机窗口,将数据整体迁移到Ignite,然后开启双写。如果没有停机窗口,则需要在灰度发布期间使用双写读补偿(回填)机制。考虑到系统稳定性,在灰度发布期间,我们通常采用只切换读流量的方式,写流量仍然双写到两个存储。等读流量完全切换后,再停止写入Redis。总结流程:1.准备阶段:部署Ignite集群,并全量迁移历史数据(如果可能)。2.灰度阶段(读):-写操作:同时写入RedisIgnite(双写)。-读操作:按照灰度比例,逐渐将读流量从Redis切换到Ignite。3.灰度阶段完成(读流量100%到Ignite):-验证Ignite的读性能数据准确性。4.停止写入Redis:-当读流量完全切换到Ignite后,观察一段时间确保稳定,然后停止写入Redis。-后续只需写入Ignite。5.移除Redis相关代码资源。注意事项:-双写时注意顺序:先写Redis还是先写Ignite?根据业务要求,如果需要强一致性,需要使用分布式事务(如两阶段提交)或者补偿机制。-由于双写可能失败,要有重试错误处理机制。-在迁移过程中,要监控两个存储的数据一致性。通过这种灰度发布策略,可以最大限度地减少迁移风险,保证系统稳定性。</think> 我将清晰说明如何通过灰度发布策略将存储层从Redis迁移到Ignite,同时保障系统稳定。 ### 灰度发布策略实施步骤 **1. 流量分流机制** 通过API网关实现请求分流(如Nginx流量切分): ```python # 伪代码示例:按用户ID哈希分流 def route_request(user_id): if user_id % 100 < gray_ratio: # 灰度比例控制 return ignite_storage.get_image() # 迁移到Ignite else: return redis_storage.get_image() # 保留Redis ``` - 初始灰度比例设为$1\%$,逐步按$1\%\rightarrow5\%\rightarrow20\%\rightarrow50\%\rightarrow100\%$阶梯式扩大 - 支持基于用户ID、设备类型等多维度分流规则 **2. 双写验证阶段** ```mermaid graph LR A[写请求] --> B[双写模块] B --> C[Redis存储] B --> D[Ignite存储] E[校验服务] -->|对比数据| C E -->|对比数据| D F[告警系统] -->|数据差异报警| E ``` - 关键操作: 1. 所有写操作同步写入RedisIgnite 2. 异步校验服务对比$Redis\_data=Ignite\_data$ 3. 差异率阈值设为$0.1\%$,超过即触发告警 **3. 指标监控体系** 需监控的核心指标: | 监控维度 | Redis指标 | Ignite指标 | 告警阈值 | |----------------|---------------------------|---------------------------|---------------| | 性能 | QPS > 5000 | 响应时间 < 100ms | 偏离基线30% | | 可靠性 | 错误率 < 0.1% | 事务成功率 > 99.9% | 连续错误 > 5 | | 数据一致性 | - | 校验差异率 < 0.01% | 超过0.1% | | 资源消耗 | 内存使用 < 80% | CPU负载 < 70% | 持续>90% 5min| **4. 熔断与回滚机制** - 动态熔断规则: $熔断触发条件 = (错误率 > 5\%) \lor (延迟 > 300ms) \lor (数据差异 > 0.5\%)$ - 回滚操作: 1. 立即将故障节点流量切回Redis 2. 执行$Ignite\_rollback\_script$清除问题数据 3. 触发自动化补偿事务 ### 关键技术实现 **1. 数据迁移工具链** ```mermaid sequenceDiagram Redis->>Kafka: 实时增量同步 Kafka->>Ignite: 批量导入 Ignite-->>Redis: 校验反馈 Note right of Ignite: 采用CRC32校验算法<br/>$checksum = \sum_{i=1}^{n} data_i \mod 2^{32}$ ``` **2. 事务一致性保障** 采用Saga模式实现分布式事务: ```python def migrate_transaction(): try: # 步骤1: Redis锁定数据 redis.lock(key, TTL=5s) # 步骤2: Ignite写入 ignite.set(key, value) # 步骤3: 提交事务 if ignite.commit() and validate_data(): redis.delete(key) # 提交成功删除Redis except Exception as e: ignite.rollback() # 异常回滚 redis.unlock(key) # 释放锁 ``` ### 最佳实践建议 1. **预热优化**:迁移前执行Ignite集群预热 $通过\ \texttt{SELECT COUNT(*)}\ \ 全表扫描加载数据到内存$ 2. **灰度批次**: - 首周:仅$1\%$非核心业务流量 - 次周:$5\%$包含核心读操作 - 第三周:$20\%$包含写操作 - 验证周期不小于$72$小时 3. **性能调优**: - 调整Ignite分区数量:$N = \log_2(cluster\_size \times 8)$ - 启用原生持久化:`DataRegionConfiguration.setPersistenceEnabled(true)` 通过该方案,某电商平台成功完成迁移,核心指标变化: ``` 迁移期间零数据丢失 | 99分位延迟仅增加18ms | 故障回滚耗时<8s ``` ### 相关问题 1. 如何设计跨数据存储的事务一致性方案? 2. 灰度发布中如何选择合适的流量切分维度? 3. Ignite集群部署有哪些关键配置优化点?[^2] 4. 数据迁移过程中如何验证全量数据一致性? 5. 熔断降级策略应如何设置阈值参数?[^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值