分布式锁
分布式锁主流方案:
1.redis
2.zookeeper
锁要求
- 避免死锁:与内存锁一样,如何避免死锁是分布式锁最基本的要求,解决方案也和内存锁如出一辙,要么通过某种机制保持获取锁的顺序是一致的,要么就是通过设置超时时间避免长期占有锁。
- 可重入:重入锁也叫递归锁,重入意味着对于同一个锁,同一个申请者在第一次获得锁之后并不需要再次获得该锁而访问需要持有该锁才可访问的其他数据实体,这样可以避免死锁的产生。
- 高性能:分布式锁的使用,意味着共享数据读写需要串行(悲观锁)。可能成为性能瓶颈。
- 高可靠:分布式锁一般以服务的形式提供,例如google的chubby,封装后的zk锁服务,需要有较高的可靠性作为保证。
Redis简介(待完善)
-
Redis 是一款开源的,基于 BSD 许可的,高级键值 (key-value) 缓存 (cache) 和存储 (store) 系统。由于 Redis 的键包括 string,hash,list,set,sorted set,bitmap 和 hyperloglog,所以常常被称为数据结构服务器。你可以在这些类型上面运行原子操作,例如,追加字符串,增加哈希中的值,加入一个元素到列表,计算集合的交集、并集和差集,或者是从有序集合中获取最高排名的元素。读写10w/s左右
-
Master/slave复制模式以及一致性hash
-
支持持久化功能RDB+AOF
-
集群(方案一):
1.Twemproxy:一致性hash
2.Sentinel:监视主从服务器,并在主服务器下线时自动进行故障转移
3.Zk:存储redis分片信息 -
集群(方案二):通过槽节点
-
分片以及一致性hash
分布式锁之redis
加锁:
释放锁:del
问题1:获得锁成功后无法释放,诸如单台系统挂掉
解决方案:设置可行的超时时间
问题2:无法可重入,即申请锁成功后,相同节点再次访问,锁失败
解决方案:对已经获得锁的进程做一个标记,标记可以是在进程内部,进程在每次加锁之前都需要访问当前内存中的标记,查看是否已经获取到锁,如果获取到则无需发出请求,直接执行操作
到底如何加锁(?)
方式一:申请锁使用setnx时,置value值为当前时间戳。在加锁时,如果存在锁已被其他进程占用,则可通过get 接口返回时间戳,与当前时间做对比,如果超过超时时间则释放锁。
带来的问题:
此时释放锁的操作可能是多个进程同时操作,会导致并发问题。
步骤
- 进程A奔溃导致无法释放锁,此时redis时间戳记录的是进程A获取到锁的时间
- 进程B 尝试获取锁,发现锁已被其他进程占用,反查当前锁的时间戳,超过超时间,则发起删除锁操作,且重新setnx获取锁。
- 进程C 尝试获取锁,发现锁已被其他进程占用,反查当前锁的时间戳,超过超时间,则发起删除锁操作,但是此时删除的锁很可能是进程B重新赋予的新值。然后,进程C重新setnx获取锁。
同一时间内,进程B与C同时获得锁。
如何真正解决同时获得锁的问题?:
在重新赋予新的时间戳的时候判断当前的缓存状态是否是之前获取的状态(时间戳是否已经被其他进程改过了,比如进程C在覆盖时间戳时可能被进程B覆盖了)。
步骤
1.进程A奔溃导致无法释放锁,此时redis时间戳记录的是进程A获取到锁的时间
2.进程B 尝试获取锁,发现锁已被其他进程占用,反查当前锁的时间戳,超过超时间,通过GetSet命令(覆盖之后返回覆盖之前的状态),判断返回的覆盖之前的时间戳是否是之前反查得到的时间戳,如果不是则说明该锁已经被其他进程获取。
3.进程C 如上。
4.同一时间内,进程C虽然覆盖了时间戳,但是并未获取该锁,因为进程C返回的覆盖之前的值与之前的反查得到的时间戳不相等。此时进程B得到新锁。
这里唯一的问题就是进程C覆盖了B写的时间戳,导致B的超时时间有所延长,考虑到出现以上场景通常都是ms级别的误差,所以对应用应该是不影响的
伪代码:
# get lock
lock = 0
while lock != 1:
timestamp = current Unix time + lock timeout + 1
lock = SETNX lock.foo timestamp
if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
break;
else:
sleep(10ms)
# do your job
do_job()
# release
if now() < GET lock.foo:
DEL lock.foo
分布式锁之zookeeper
Zookeeper能做什么
1.分布式统一资源配置(如线上开关,应用信息
应用集群无需重启,即可异步监听主节点信息,修改配置获取节点信息,配置统一调整
应用:tbschedule,hbase,Hadoop,dubbo等
2.集群master选取以及master/salve切换(容灾
场景:后台worker集群,用户定时刷新缓存,如果一台机器down机器一组机器进行选取(sequence id最小)
应用:master-worker
Hadoop,使用Zookeeper的事件处理确保整个集群只有一个NameNode,存储配置信息等.
HBase,使用Zookeeper的事件处理确保整个集群只有一个HMaster,察觉HRegionServer联机和宕机,存储访问控制列表等.
3.分布式锁
- zk各节点说明
- 应用场景:zookeeperleader选取
- 应用场景:master选取
- 应用场景:统一资源配置
补充说明
1.节点类型znode
临时,永久,临时序列,永久序列临时节点(Ephemeral Nodes):当客户端和服务端连接断掉,该节点将被删除。
永久节点(Persistent Nodes):客户端和服务端连接断掉,该节点也不会被删除。
2.Master选举过程中默认使用最小编号
3.Leader选举超过半数节点存活且,最大mxid,算法paxos
1,2,3,4,5 机器3默认启动为leader
zk性能总结:
1.写入(set)数据时,单个线程执行是最快的,而读取(get)数据则是4或5个线程并发是最快的。
2.每个节点存储的数据不能超过1M(在Zookeeper默认配置的情况下,如果写入的数据为1M会报错,无法写入数据,所以所有应用写入的数据必须<1M)
3.单个写入数据的体积越大,处理速度越慢,响应时间越长。
4.建议压缩json数据。
5.测试过程中产生的log文件对磁盘的消耗和占用较大,建议定时删除历史log和shapshot
重点来了zk分布式锁
思路:
利用zookeeper的EPHEMERAL_SEQUENTIAL类型节点及watcher机制,来简单实现分布式锁。
主要思想
1.开启10个线程,在disLocks节点下各自创建名为sub的EPHEMERAL_SEQUENTIAL节点;
2.获取disLocks节点下所有子节点,排序,如果自己的节点编号最小,则获取锁;
3.否则watch排在自己前面的节点,监听到其删除后,进入第2步(重新检测排序是防止监听的节点发生连接失效,导致的节点删除情况);
4.删除自身sub节点,释放连接;
最重要的小事上代码
public class DistributedLock implements Watcher{
private int threadId;
private ZooKeeper zk = null;
private String selfPath;
private String waitPath;
private String LOG_PREFIX_OF_THREAD;
private static final int SESSION_TIMEOUT = 10000;
private static final String GROUP_PATH = "/disLocks";
private static final String SUB_PATH = "/disLocks/sub";
private static final String CONNECTION_STRING = "192.168.*.*:2181";
private static final int THREAD_NUM = 10;
//确保连接zk成功;
private CountDownLatch connectedSemaphore = new CountDownLatch(1);
//确保所有线程运行结束;
private static final CountDownLatch threadSemaphore = new CountDownLatch(THREAD_NUM);
private static final Logger LOG = LoggerFactory.getLogger(AllZooKeeperWatcher.class);
public DistributedLock(int id) {
this.threadId = id;
LOG_PREFIX_OF_THREAD = "【第"+threadId+"个线程】";
}
public static void main(String[] args) {
for(int i=0; i < THREAD_NUM; i++){
final int threadId = i+1;
new Thread(){
@Override
public void run() {
try{
DistributedLock dc = new DistributedLock(threadId);
dc.createConnection(CONNECTION_STRING, SESSION_TIMEOUT);
//GROUP_PATH不存在的话,由一个线程创建即可;
synchronized (threadSemaphore){
dc.createPath(GROUP_PATH, "该节点由线程" + threadId + "创建", true);
}
dc.getLock();
} catch (Exception e){
LOG.error("【第"+threadId+"个线程】 抛出的异常:");
e.printStackTrace();
}
}
}.start();
}
try {
threadSemaphore.await();
LOG.info("所有线程运行结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 获取锁
* @return
*/
private void getLock() throws KeeperException, InterruptedException {
selfPath = zk.create(SUB_PATH,null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
LOG.info(LOG_PREFIX_OF_THREAD+"创建锁路径:"+selfPath);
if(checkMinPath()){
getLockSuccess();
}
}
/**
* 创建节点
* @param path 节点path
* @param data 初始数据内容
* @return
*/
public boolean createPath( String path, String data, boolean needWatch) throws KeeperException, InterruptedException {
if(zk.exists(path, needWatch)==null){
LOG.info( LOG_PREFIX_OF_THREAD + "节点创建成功, Path: "
+ this.zk.create( path,
data.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT )
+ ", content: " + data );
}
return true;
}
/**
* 创建ZK连接
* @param connectString ZK服务器地址列表
* @param sessionTimeout Session超时时间
*/
public void createConnection( String connectString, int sessionTimeout ) throws IOException, InterruptedException {
zk = new ZooKeeper( connectString, sessionTimeout, this);
connectedSemaphore.await();
}
/**
* 获取锁成功
*/
public void getLockSuccess() throws KeeperException, InterruptedException {
if(zk.exists(this.selfPath,false) == null){
LOG.error(LOG_PREFIX_OF_THREAD+"本节点已不在了...");
return;
}
LOG.info(LOG_PREFIX_OF_THREAD + "获取锁成功,赶紧干活!");
Thread.sleep(2000);
LOG.info(LOG_PREFIX_OF_THREAD + "删除本节点:"+selfPath);
zk.delete(this.selfPath, -1);
releaseConnection();
threadSemaphore.countDown();
}
/**
* 关闭ZK连接
*/
public void releaseConnection() {
if ( this.zk !=null ) {
try {
this.zk.close();
} catch ( InterruptedException e ) {}
}
LOG.info(LOG_PREFIX_OF_THREAD + "释放连接");
}
/**
* 检查自己是不是最小的节点
* @return
*/
public boolean checkMinPath() throws KeeperException, InterruptedException {
List<String> subNodes = zk.getChildren(GROUP_PATH, false);
Collections.sort(subNodes);
int index = subNodes.indexOf( selfPath.substring(GROUP_PATH.length()+1));
switch (index){
case -1:{
LOG.error(LOG_PREFIX_OF_THREAD+"本节点已不在了..."+selfPath);
return false;
}
case 0:{
LOG.info(LOG_PREFIX_OF_THREAD+"子节点中,我果然是老大"+selfPath);
return true;
}
default:{
this.waitPath = GROUP_PATH +"/"+ subNodes.get(index - 1);
LOG.info(LOG_PREFIX_OF_THREAD+"获取子节点中,排在我前面的"+waitPath);
try{
zk.getData(waitPath, true, new Stat());
return false;
}catch(KeeperException e){
if(zk.exists(waitPath,false) == null){
LOG.info(LOG_PREFIX_OF_THREAD+"子节点中,排在我前面的"+waitPath+"已失踪,幸福来得太突然?");
return checkMinPath();
}else{
throw e;
}
}
}
}
}
@Override
public void process(WatchedEvent event) {
if(event == null){
return;
}
Event.KeeperState keeperState = event.getState();
Event.EventType eventType = event.getType();
if ( Event.KeeperState.SyncConnected == keeperState) {
if ( Event.EventType.None == eventType ) {
LOG.info( LOG_PREFIX_OF_THREAD + "成功连接上ZK服务器" );
connectedSemaphore.countDown();
}else if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
LOG.info(LOG_PREFIX_OF_THREAD + "收到情报,排我前面的家伙已挂,我是不是可以出山了?");
try {
if(checkMinPath()){
getLockSuccess();
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else if ( Event.KeeperState.Disconnected == keeperState ) {
LOG.info( LOG_PREFIX_OF_THREAD + "与ZK服务器断开连接" );
} else if ( Event.KeeperState.AuthFailed == keeperState ) {
LOG.info( LOG_PREFIX_OF_THREAD + "权限检查失败" );
} else if ( Event.KeeperState.Expired == keeperState ) {
LOG.info( LOG_PREFIX_OF_THREAD + "会话失效" );
}
}
}
总结
目前主流使用redis以及tair
对于zookeeper分布式锁在生产环境上使用者也比较多知识个人没使用
zk几点注意事项:
1.链接数过多容易产生事故
2.不适合存储超过100M以上的内容
3.机器集群切记不要太多,否则在leader与follower之间复制可能会存在超时等问题,最终一致性较差