当前位置: 首页 > 后端技术 > PHP

分别使用数据库、Redis、ZK实现分布式锁!

时间:2023-03-29 13:53:25 PHP

分布式锁的三种实现:基于数据库的分布式锁;基于缓存的分布式锁(Redis等);基于Zookeeper的分布式锁;基于数据库的分布式锁悲观锁利用select…where…forupdate独占锁注意:其他附加功能与实现1基本相同,这里需要注意的是“wherename=lock”,name字段必须有索引,否则表将被锁定。在某些情况下,比如小表,mysql优化器不会使用这个索引,导致表锁定问题。乐观锁所谓的乐观锁和前面的最大的区别就是它是基于CAS思想的。不互斥,不会导致锁等待消耗资源。运行过程中,认为没有并发冲突,只有更新版本失败后才能检测到。我们的恐慌抢购和限时抢购就是使用这种方式来防止超卖。乐观锁是通过增加自增的版本号字段来实现的。基于缓存的分布式锁(Redis等);如果key存在,则什么也不做,返回0。(2)expireexpirekey超时:为key设置一个超时时间,单位是秒,超过这个时间会自动释放锁,避免死锁。(3)deleteddeletekey:deletekey在使用Redis实现分布式锁时,主要用到这三个命令。实现思路:(1)在获取锁的时候,使用setnx加锁,使用expire命令为锁添加超时时间,超时后自动释放锁,锁的值为一个随机生成的UUID,在加锁的时候通过这个判断释放。(2)在获取锁的时候,也设置了一个获取超时时间。如果超过这个时间,将放弃获取锁。(3)释放锁时,通过UUID判断是否是锁。如果是锁,则执行delete释放锁。分布式锁简单实现代码/***分布式锁简单实现代码4*/publicclassDistributedLock{privatefinalJedisPooljedisPool;publicDistributedLock(JedisPooljedisPool){this.jedisPool=jedisPool;}/***lock*@paramlockName锁键*@paramacquireTimeout获取超时*@paramtimeout锁超时*@return锁标识符*/publicStringlockWithTimeout(StringlockName,longacquireTimeout,longtimeout){Jedisconn=null;字符串retIdentifier=null;try{//获取连接conn=jedisPool.getResource();//随机生成一个值Stringidentifier=UUID.randomUUID().toString();//锁名,即键值StringlockKey="lock:"+lockName;//超时时间,超过这个时间后会自动释放锁intlockExpire=(int)(timeout/1000);//获取锁的超时时间,超时后将放弃获取锁longend=System.currentTimeMillis()+acquireTimeout;而(系统em.currentTimeMillis()结果=transaction.exec();如果(结果==null){继续;}retFlag=true;}康涅狄格州。取消观看();休息;}}catch(JedisExceptione){e.printStackTrace();}最后{if(conn!=null){conn.close();}}返回retFlag;}}测试实现的分布式锁示例,使用50个线程模拟某商品闪购,使用-运算符对商品进行减价。结果是可以看到是不是锁态模拟秒杀服务,里面配置了jedis线程池,在初始化的时候传给分布式锁供其使用。公共类服务{privatestaticJedisPoolpool=null;privateDistributedLocklock=newDistributedLock(pool);诠释n=500;static{JedisPoolConfig配置=newJedisPoolConfig();//设置最大连接数config.setMaxTotal(200);//设置最大空闲数config.setMaxIdle(8);//设置最大等待时间config.setMaxWaitMillis(1000*100);//借用jedis实例时是否需要验证,如果为true,则所有jedis实例都可用config.setTestOnBorrow(true);pool=newJedisPool(config,"127.0.0.1",6379,3000);}publicvoidseckill(){//释放锁时返回锁的值进行判断Stringidentifier=lock.lockWithTimeout("resource",5000,1000);System.out.println(Thread.currentThread().getName()+"获得锁");System.out.println(--n);锁。releaseLock("资源",标识符);}}为秒杀服务模拟一个线程;公共类ThreadA扩展线程{私人服务服务;publicThreadA(Serviceservice){this.service=service;}@Overridepublicvoidrun(){service.seckill();}}publicclassTest{publicstaticvoidmain(String[]args){Serviceservice=newService();for(inti=0;i<50;i++){ThreadAthreadA=newThreadA(service);threadA.start();}}}结果如下,结果排序:if把使用锁的部分注释掉:publicvoidseckill(){//释放锁时返回锁的值用于判断//Stringidentifier=lock.lockWithTimeout("资源",5000,1000);System.out.println(Thread.currentThread().getName()+"获得锁");System.out.println(--n);//lock.releaseLock("resource",identifier);}从结果可以看出,有一些是异步的:基于ZooKeeper实现分布式锁。ZooKeeper是一个为分布式应用程序提供一致服务的开源组件。它的内部是一个层次化的文件系统目录树结构。规定同一个目录下只能有一个唯一的文件系统。文件名基于ZooKeeper实现分布式锁的步骤如下:(1)创建目录mylock;(2)线程A要获取锁,在mylock目录下创建一个临时的顺序节点;(3)获取mylock目录下的所有子节点,然后获取如果比自己小的兄弟节点不存在,则说明当前线程序号最小,获取锁;(4)线程B获取所有节点,判断自己不是最小节点,设置比自己小的监听节点;(5)线程B处理完A,删除自己的结点,线程B监听change事件,判断是否为最小结点,如果是则获取锁。这里推荐Curator,一个Apache开源库,它是一个ZooKeeper的客户端。Curator提供的InterProcessMutex是分布式锁的一种实现。acquire方法用于获取锁,release方法用于释放锁。实际源码如下:importlombok.extern.slf4j.Slf4j;导入org.apache.commons.lang.StringUtils;导入org.apache.curator.framework.CuratorFramework;导入org.apache.curator.framework.CuratorFrameworkFactory;导入org.apache.curator.retry.RetryNTimes;导入org.apache.zookeeper.CreateMode;导入org.apache.zookeeper.data.Stat;导入org.springframework.beans.factory.annotation.Value;导入org.springframework.context.annotation.Bean;导入org.springframework.stereotype.Component;/***分布式锁Zookeeper实现**/@Slf4j@ComponentpublicclassZkLockimplementsDistributionLock{privateStringzkAddress="zk_adress";privatestaticfinalStringroot="packageroot";私有CuratorFrameworkzkClient;privatefinalStringLOCK_PREFIX="/lock_";@BeanpublicDistributionLockinitZkLock(){if(StringUtils.isBlank(root)){thrownewRuntimeException("zookeeper'root'不能为空");}zkClient=CuratorFrameworkFactory.builder().connectString(zkAddress).retryPolicy(newRetryNTimes(2000,20000)).namespace(root).build();zkClient.start();归还这个;}publicbooleantryLock(StringlockName){lockName=LOCK_PREFIX+lockName;布尔锁定=真;尝试{Statstat=zkClient.checkExists().forPath(lockName);if(stat==null){log.info("tryLock:{}",lockName);stat=zkClient.checkExists().forPath(lockName);if(stat==null){zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(lockName,"1".getBytes());}else{log.warn("仔细检查stat.version:{}",stat.getAversion());锁定=假;}}else{log.warn("检查stat.version:{}",stat.getAversion());锁定=假;}}catch(Exceptione){locked=false;}返回锁定;}publicbooleantryLock(Stringkey,longtimeout){returnfalse;}publicvoidrelease(StringlockName){lockName=LOCK_PREFIX+lockName;尝试{zkClient.delete().guaranteed().deletingChildrenIfNeeded().forPath(lockName);log.info("release:{}",lockName);}catch(Exceptione){log.error("删除",e);}}publicvoidsetZkAddress(StringzkAddress){this.zkAddress=zkAddress;优点:高可用、可重入、阻塞锁等特性,可以解决失败死锁问题。缺点:由于需要频繁创建和删除节点,性能不如Redis方式。总结数据库分布式锁实现的缺点:1、db操作性能差,存在锁表风险;2、非阻塞操作失败后,需要轮询,占用CPU资源;3.长期不提交或长期轮询,可能会占用较多的连接资源Redis(缓存)分布式锁实现缺点:1.锁删除失败过期时间不易控制2.非阻塞,后操作失败,需要轮询,占用CPU资源;ZK分布式锁实现缺点:性能不如redis实现。主要原因是所有写操作(获取和释放锁)都需要在leader上执行,然后同步给follower。简而言之:ZooKeeper具有更好的性能和可靠性。从易懂性角度(从低到高)数据库>缓存>Zookeeper从实现复杂度角度(从低到高)Zookeeper>=缓存>数据库从性能角度(从高到低)缓存>Zookeeper>=Databasefromreliabilitypoint(从高到低)Zookeeper>Cache>Database