更新时间:2021-11-30 07:00:23
云栖号快速入门:【点击查看更多云产品快速入门】
不知道怎么入门?这里分分钟解决新手入门等基础问题,可快速完成产品配置操作!
分布式锁是大型应用中最常见的功能之一,基于Redis实现分布式锁的方式有很多。本章节先介绍并分析常见的分布式锁实现方式,之后结合阿里巴巴集团在使用云数据库Redis企业版和分布式锁方面的业务经验,介绍使用Redis企业版实现高性能分布式锁的实践方案。
分布式锁及其应用场景
应用开发时,如果需要在同进程内的不同线程并发访问某项资源,可以使用各种互斥锁、读写锁;如果一台主机上的多个进程需要并发访问某项资源,则可以使用进程间同步的原语,例如信号量、管道、共享内存等。但如果多台主机需要同时访问某项资源,就需要使用一种在全局可见并具有互斥性的锁了。这种锁就是分布式锁,可以在分布式场景中对资源加锁,避免竞争资源引起的逻辑错误。
分布式锁的特性
使用原生Redis实现分布式锁
SET resource_1 random_value NX EX 5
表 1. 关键选项说明
示例代码为resource_1这个key设置了5秒的过期时间,如果客户端不释放这个key,5秒后key将过期,锁就会被系统回收,此时其它客户端就能够再次为资源加锁并访问资源了。
(1).t1时刻,App1设置了分布式锁resource_1,过期时间为3秒。
(2).App1由于程序慢等原因等待超过了3秒,而resource_1已经在t2时刻被释放。
(3).t3时刻,App2获得这个分布式锁。
(4).App1从等待中恢复,在t4时刻运行将App2持有的分布式锁释放了。
从上述过程可以看出,一个客户端设置的锁,必须由自己解开。因此客户端需要先使用GET命令确认锁是不是自己设置的,然后再使用DEL解锁。在Redis中通常需要用Lua脚本来实现自锁自解:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("expire",KEYS[1], ARGV[2])
else
return 0
end
使用Redis企业版实现分布式锁
使用Redis企业版性能增强型实例的String增强命令,无需Lua即可实现分布式锁。
SET resource_1 random_value NX EX 5
/* if (GET(resource_1) == my_random_value) DEL(resource_1) */
CAD resource_1 my_random_value
CAS resource_1 my_random_value my_random_value EX 10
基于Jedis的示例代码
enum TairCommand implements ProtocolCommand {
CAD("CAD"), CAS("CAS");
private final byte[] raw;
TairCommand(String alt) {
raw = SafeEncoder.encode(alt);
}
@Override
public byte[] getRaw() {
return raw;
}
}
public boolean acquireDistributedLock(Jedis jedis,String resourceKey, String randomValue, int expireTime) {
SetParams setParams = new SetParams();
setParams.nx().ex(expireTime);
String result = jedis.set(resourceKey,randomValue,setParams);
return "OK".equals(result);
}
public boolean releaseDistributedLock(Jedis jedis,String resourceKey, String randomValue) {
jedis.getClient().sendCommand(TairCommand.CAD,resourceKey,randomValue);
Long ret = jedis.getClient().getIntegerReply();
return 1 == ret;
}
public boolean renewDistributedLock(Jedis jedis,String resourceKey, String randomValue, int expireTime) {
jedis.getClient().sendCommand(TairCommand.CAS,resourceKey,randomValue,randomValue,"EX",String.valueOf(expireTime));
Long ret = jedis.getClient().getIntegerReply();
return 1 == ret;
}
如何保障一致性
Redis的主从同步(replication)是异步进行的,如果向master发送请求修改了数据后master突然出现异常,发生高可用切换,缓冲区的数据可能无法同步到新的master(原replica)上,导致数据不一致。如果丢失的数据跟分布式锁有关,则会导致锁的机制出现问题,从而引起业务异常。下文介绍三种保障一致性的方法。
红锁的问题在于:
(1).加锁和解锁的延迟较大。
(2).难以在集群版或者标准版(主从架构)的Redis实例中实现。
(3).占用的资源过多,为了实现红锁,需要创建多个互不相关的云Redis实例或者自建Redis。
SET resource_1 random_value NX EX 5
WAIT 1 5000
使用以上代码,客户端在加锁后会等待数据成功同步到replica才继续进行其它操作,最大等待时间为5000毫秒。执行WAIT命令后如果返回结果是1则表示同步成功,无需担心数据不一致。相比红锁,这种实现方法极大地降低了成本。
需要注意的是:
(1).WAIT只会阻塞发送它的客户端,不影响其它客户端。
(2).WAIT返回正确的值表示设置的锁成功同步到了replica,但如果在正常返回前发生高可用切换,数据还是可能丢失,此时WAIT只能用来提示同步可能失败,无法保证数据不丢失。您可以在WAIT返回异常值后重新加锁或者进行数据校验。
(3).解锁不一定需要使用WAIT,因为锁只要存在就能保持互斥,延迟删除不会导致逻辑问题。
在不考虑组合方案的情况下:
(1).使用红锁最大优势是Redis节点越多则一致性越强。
(2).使用WAIT命令最大优势是实现成本低。
如果使用阿里云数据库Redis企业版:
(1).其特有的高可用HA和数据持久化机制能够有效保护数据安全、确保服务的稳定性,不使用多Redis节点或WAIT命令也能提供较高的一致性。
(2).性能增强型实例的CAS/CAD命令可以极大降低分布式锁的开发和管理成本,提升锁的性能。
(3).性能增强型实例的多线程性能增强特性使其能够提供三倍于原生Redis的性能,即使是大并发的分布式锁也不会影响正常的Redis服务。
本文来自 阿里云文档中心 云数据库Redis 高性能分布式锁
【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/zhibo立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK