Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
一个基于Redis实现的分布式工具,有基本分布式对象和高级又抽象的分布式服务,为每个试图再造分布式轮子的程序员带来了大部分分布式问题的解决办法。
Redisson和Jedis、Lettuce有什么区别?倒也不是雷锋和雷锋塔
Redisson和它俩的区别就像一个用鼠标操作图形化界面,一个用命令行操作文件。Redisson是更高层的抽象,Jedis和Lettuce是Redis命令的封装。
Jedis是Redis官方推出的用于通过Java连接Redis客户端的一个工具包,提供了Redis的各种命令支持
Lettuce是一种可扩展的线程安全的 Redis 客户端,通讯框架基于Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。
Redisson是架设在Redis基础上,通讯基于Netty的综合的、新型的中间件,企业级开发中使用Redis的最佳范本
Jedis把Redis命令封装好,Lettuce则进一步有了更丰富的Api,也支持集群等模式。但是两者也都点到为止,只给了你操作Redis数据库的脚手架,而Redisson则是基于Redis、Lua和Netty建立起了成熟的分布式解决方案,甚至redis官方都推荐的一种工具集。
分布式锁是并发业务下的刚需,虽然实现五花八门:ZooKeeper有Znode顺序节点,数据库有表级锁和乐/悲观锁,Redis有setNx,但是殊途同归,最终还是要回到互斥上来,本篇介绍Redisson,那就以redis为例。
以Spring Data Redis为例,用RedisTemplate来操作Redis(setIfAbsent已经是setNx + expire的合并命令),如下:
// 加锁 public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) { return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit); } // 解锁,防止删错别人的锁,以uuid为value校验是否自己的锁 public void unlock(String lockName, String uuid) { if(uuid.equals(redisTemplate.opsForValue().get(lockName)){ redisTemplate.opsForValue().del(lockName); } } // 结构 if(tryLock){ // todo }finally{ unlock; }
简单1.0版本完成,聪明的小张一眼看出,这是锁没错,但get和del操作非原子性,并发一旦大了,无法保证进程安全。于是小张提议,用Lua脚本
Lua脚本是redis已经内置的一种轻量小巧语言,其执行是通过redis的eval/evalsha命令来运行,把操作封装成一个Lua脚本,如论如何都是一次执行的原子操作。
于是2.0版本通过Lua脚本删除
lockDel.lua如下:
if redis.call('get', KEYS[1]) == ARGV[1] then -- 执行删除操作 return redis.call('del', KEYS[1]) else -- 不成功,返回0 return 0 end
delete操作时执行Lua命令:
// 解锁脚本 DefaultRedisScript unlockScript = new DefaultRedisScript(); unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua"))); // 执行lua脚本解锁 redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value); 2.0似乎更像一把锁,但好像又缺少了什么,小张一拍脑袋,synchronized和ReentrantLock都很丝滑,因为他们都是可重入锁,一个线程多次拿锁也不会死锁,我们需要可重入。 2.4 怎么保证可重入? 重入就是,同一个线程多次获取同一把锁是允许的,不会造成死锁,这一点synchronized偏向锁提供了很好的思路,synchronized的实现重入是在JVM层面,JAVA对象头MARK WORD中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS。 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。 再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放 2.5 可重入锁 仿造该方案,我们需改造Lua脚本: 1.需要存储 锁名称lockName、获得该锁的线程id和对应线程的进入次数count 2.加锁 每次线程获取锁时,判断是否已存在该锁 不存在 设置hash的key为线程id,value初始化为1 设置过期时间 返回获取锁成功true 存在 继续判断是否存在当前线程id的hash key 存在,线程key的value + 1,重入次数增加1,设置过期时间 不存在,返回加锁失败 3.解锁 每次线程来解锁时,判断是否已存在该锁 存在 是否有该线程的id的hash key,有则减1,无则返回解锁失败 减1后,判断剩余count是否为0,为0则说明不再需要这把锁,执行del命令删除 2.5.1 存储结构 为了方便维护这个对象,我们用Hash结构来存储这些字段。Redis的Hash类似Java的HashMap,适合存储对象。 hset lockname1 threadId 1 设置一个名字为lockname1的hash结构,该hash结构key为threadId,值value为1 hget lockname1 threadId 获取lockname1的threadId的值 存储结构为: lockname 锁名称 key1: threadId 唯一键,线程id value1: count 计数器,记录该线程获取锁的次数 redis中的结构 2.5.2 计数器的加减 当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减 判断一个redis key是否存在,可以用exists,而判断一个hash的key是否存在,可以用hexists 而redis也有hash自增的命令hincrby 每次自增1时 hincrby lockname1 threadId 1,自减1时 hincrby lockname1 threadId -1 2.5.3 解锁的判断 当一把锁不再被需要了,每次解锁一次,count减1,直到为0时,执行删除 综合上述的存储结构和判断流程,加锁和解锁Lua如下 加锁 lock.lua: local key = KEYS[1]; local threadId = ARGV[1]; local releaseTime = ARGV[2]; -- lockname不存在 if(redis.call('exists', key) == 0) then redis.call('hset', key, threadId, '1'); redis.call('expire', key, releaseTime); return 1; end; -- 当前线程已id存在 if(redis.call('hexists', key, threadId) == 1) then redis.call('hincrby', key, threadId, '1'); redis.call('expire', key, releaseTime); return 1; end; return 0; 解锁 unlock.lua: local key = KEYS[1]; local threadId = ARGV[1]; -- lockname、threadId不存在 if (redis.call('hexists', key, threadId) == 0) then return nil; end; -- 计数器-1 local count = redis.call('hincrby', key, threadId, -1); -- 删除lock if (count == 0) then redis.call('del', key); return nil; end; 代码: /** * @description 原生redis实现分布式锁 **/ @Getter @Setter public class RedisLock { private RedisTemplate redisTemplate; private DefaultRedisScript lockScript; private DefaultRedisScript unlockScript; public RedisLock(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; // 加载加锁的脚本 lockScript = new DefaultRedisScript<>(); this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua"))); this.lockScript.setResultType(Long.class); // 加载释放锁的脚本 unlockScript = new DefaultRedisScript<>(); this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua"))); } /** * 获取锁 */ public String tryLock(String lockName, long releaseTime) { // 存入的线程信息的前缀 String key = UUID.randomUUID().toString(); // 执行脚本 Long result = (Long) redisTemplate.execute( lockScript, Collections.singletonList(lockName), key + Thread.currentThread().getId(), releaseTime); if (result != null && result.intValue() == 1) { return key; } else { return null; } } /** * 解锁 * @param lockName * @param key */ public void unlock(String lockName, String key) { redisTemplate.execute(unlockScript, Collections.singletonList(lockName), key + Thread.currentThread().getId() ); } } 至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。 严谨的小张觉得虽然当个普通互斥锁,已经稳稳够用,可是业务里总是又很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题。 而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。 小张不是杠精,因为库存操作总有这样那样的特殊。 所以我们希望在这种情况时,可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果,这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约。 读写分离也是常见,一个读多写少的业务为了性能,常常是有读锁和写锁的。 而此刻的扩展已经超出了一把简单轮子的复杂程度,光是处理续约,就够小张喝一壶,何况在性能(锁的最大等待时间)、优雅(无效锁申请)、重试(失败重试机制)等方面还要下功夫研究。 在小张苦思冥想时,旁边的小白凑过来看了看小张,很好奇,都2021年了,为什么不直接用redisson呢? Redisson就有这把你要的锁。 3 Redisson分布式锁 号称简单的Redisson分布式锁的使用姿势是什么? 3.1 依赖 org.redisson redisson 3.13.6 org.redisson redisson-spring-boot-starter 3.13.6 3.2 配置 @Configuration public class RedissionConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.password}") private String password; private int port = 6379; @Bean public RedissonClient getRedisson() { Config config = new Config(); config.useSingleServer(). setAddress("redis://" + redisHost + ":" + port). setPassword(password); config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); } } 3.3 启用分布式锁 @Resource private RedissonClient redissonClient; RLock rLock = redissonClient.getLock(lockName); try { boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS); if (isLocked) { // TODO } } catch (Exception e) { rLock.unlock(); } 简洁明了,只需要一个RLock,既然推荐Redisson,就往里面看看他是怎么实现的。 4 RLock RLock是Redisson分布式锁的最核心接口,继承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson执行异步实现的核心逻辑,也是Netty发挥的主要阵地。 RLock如何加锁? 从RLock进入,找到RedissonLock类,找到tryLock方法再递进到干事的tryAcquireOnceAsync方法,这是加锁的主要代码(版本不一此处实现有差别,和最新3.15.x有一定出入,但是核心逻辑依然未变。此处以3.13.6为例) private RFuture tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else { RFuture ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } } 此处出现leaseTime时间判断的2个分支,实际上就是加锁时是否设置过期时间,未设置过期时间(-1)时则会有watchDog的锁续约(下文),一个注册了加锁事件的续约任务。我们先来看有过期时间tryLockInnerAsync部分, evalWriteAsync是eval命令执行lua的入口 RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); } 这里揭开真面目,eval命令执行Lua脚本的地方,此处的Lua脚本展开 -- 不存在该key时 if (redis.call('exists', KEYS[1]) == 0) then -- 新增该锁并且hash中该线程id对应的count置1 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间 redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 存在该key 并且 hash中线程id的key也存在 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 线程重入次数++ redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]); 和前面我们写自定义的分布式锁的脚本几乎一致,看来redisson也是一样的实现,具体参数分析: // keyName KEYS[1] = Collections.singletonList(this.getName()) // leaseTime ARGV[1] = this.internalLockLeaseTime // uuid+threadId组合的唯一值 ARGV[2] = this.getLockName(threadId) 总共3个参数完成了一段逻辑: 判断该锁是否已经有对应hash表存在, • 没有对应的hash表:则set该hash表中一个entry的key为锁名称,value为1,之后设置该hash表失效时间为leaseTime • 存在对应的hash表:则将该lockName的value执行+1操作,也就是计算进入次数,再设置失效时间leaseTime • 最后返回这把锁的ttl剩余时间 也和上述自定义锁没有区别 既然如此,那解锁的步骤也肯定有对应的-1操作,再看unlock方法,同样查找方法名,一路到 protected RFuture unlockInnerAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)}); } 掏出Lua部分 -- 不存在key if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; -- 计数器 -1 local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then -- 过期时间重设 redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else -- 删除并发布解锁消息 redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil; 该Lua KEYS有2个Arrays.asList(getName(), getChannelName()) name 锁名称 channelName,用于pubSub发布消息的channel名称 ARGV变量有三个LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId) LockPubSub.UNLOCK_MESSAGE,channel发送消息的类别,此处解锁为0 internalLockLeaseTime,watchDog配置的超时时间,默认为30s lockName 这里的lockName指的是uuid和threadId组合的唯一值 步骤如下: 1.如果该锁不存在则返回nil; 2.如果该锁存在则将其线程的hash key计数器-1, 3.计数器counter>0,重置下失效时间,返回0;否则,删除该锁,发布解锁消息unlockMessage,返回1; 其中unLock的时候使用到了Redis发布订阅PubSub完成消息通知。 而订阅的步骤就在RedissonLock的加锁入口的lock方法里 long threadId = Thread.currentThread().getId(); Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId); if (ttl != null) { // 订阅 RFuture future = this.subscribe(threadId); if (interruptibly) { this.commandExecutor.syncSubscriptionInterrupted(future); } else { this.commandExecutor.syncSubscription(future); } // 省略 当锁被其他线程占用时,通过监听锁的释放通知(在其他线程通过RedissonLock释放锁时,会通过发布订阅pub/sub功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。 4.1 解锁消息 为了一探究竟通知了什么,通知后又做了什么,进入LockPubSub。 这里只有一个明显的监听方法onMessage,其订阅和信号量的释放都在父类PublishSubscribe,我们只关注监听事件的实际操作 protected void onMessage(RedissonLockEntry value, Long message) { Runnable runnableToExecute; if (message.equals(unlockMessage)) { // 从监听器队列取监听线程执行监听回调 runnableToExecute = (Runnable)value.getListeners().poll(); if (runnableToExecute != null) { runnableToExecute.run(); } // getLatch()返回的是Semaphore,信号量,此处是释放信号量 // 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁 value.getLatch().release(); } else if (message.equals(readUnlockMessage)) { while(true) { runnableToExecute = (Runnable)value.getListeners().poll(); if (runnableToExecute == null) { value.getLatch().release(value.getLatch().getQueueLength()); break; } runnableToExecute.run(); } } } 发现一个是默认解锁消息,一个是读锁解锁消息,因为redisson是有提供读写锁的,而读写锁读读情况和读写、写写情况互斥情况不同,我们只看上面的默认解锁消息unlockMessage分支 LockPubSub监听最终执行了2件事 runnableToExecute.run() 执行监听回调 value.getLatch().release(); 释放信号量 Redisson通过LockPubSub监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁。 这时再回来看tryAcquireOnceAsync另一分支 private RFuture tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else { RFuture ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } } 可以看到,无超时时间时,在执行加锁操作后,还执行了一段费解的逻辑 ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }) } } }) 此处涉及到Netty的Future/Promise-Listener模型,Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解 在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。 这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务。 这段定时任务的就是watchDog的核心。 4.2 锁续约 查看RedissonLock.this.scheduleExpirationRenewal(threadId) private void scheduleExpirationRenewal(long threadId) { RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry(); RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry); if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); this.renewExpiration(); } } private void renewExpiration() { RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName()); if (ee != null) { Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() { public void run(Timeout timeout) throws Exception { RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName()); if (ent != null) { Long threadId = ent.getFirstThreadId(); if (threadId != null) { RFuture future = RedissonLock.this.renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e); } else { if (res) { RedissonLock.this.renewExpiration(); } } }); } } } }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); ee.setTimeout(task); } } 拆分来看,这段连续嵌套且冗长的代码实际上做了几步 • 添加一个netty的Timeout回调任务,每(internalLockLeaseTime / 3)毫秒执行一次,执行的方法是renewExpirationAsync • renewExpirationAsync重置了锁超时时间,又注册一个监听器,监听回调又执行了renewExpiration renewExpirationAsync 的Lua如下 protected RFuture renewExpirationAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); } if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0; 重新设置了超时时间。 Redisson加这段逻辑的目的是什么? 目的是为了某种场景下保证业务不影响,如任务执行超时但未结束,锁已经释放的问题。 当一个线程持有了一把锁,由于并未设置超时时间leaseTime,Redisson默认配置了30S,开启watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁。 这就是Redisson的锁续约,也就是WatchDog实现的基本思路。 4.3 流程概括 通过整体的介绍,流程简单概括: A、B线程争抢一把锁,A获取到后,B阻塞 B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息 A操作完成释放了锁,B线程收到订阅消息通知 B被唤醒开始继续抢锁,拿到锁 详细加锁解锁流程总结如下图: 5 公平锁 以上介绍的可重入锁是非公平锁,Redisson还基于Redis的队列(List)和ZSet实现了公平 5.1 公平的定义是什么? 公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少 5.2 FairSync 回顾JUC的ReentrantLock公平锁的实现 /** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } } AQS已经提供了整个实现,是否公平取决于实现类取出节点逻辑是否顺序取 AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,通过内置FIFO队列来完成资源获取线程的排队工作,他自身没有实现同步接口,仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用(上图),支持独占和共享获取,这是基于模版方法模式的一种设计,给公平/非公平提供了土壤。 我们用2张图来简单解释AQS的等待流程(出自《JAVA并发编程的艺术》) 一张是同步队列(FIFO双向队列)管理 获取同步状态失败(抢锁失败)的线程引用、等待状态和前驱后继节点的流程图 一张是独占式获取同步状态的总流程,核心acquire(int arg)方法调用流程 可以看出锁的获取流程 AQS维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋,移出队列或停止自旋的条件是前驱节点为头节点切成功获取了同步状态。 而比较另一段非公平锁类NonfairSync可以发现,控制公平和非公平的关键代码,在于hasQueuedPredecessors方法。 static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } NonfairSync减少了了hasQueuedPredecessors判断条件,该方法的作用就是 查看同步队列中当前节点是否有前驱节点,如果有比当前线程更早请求获取锁则返回true。 保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则 5.2.1 为什么JUC以默认非公平锁呢? 因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。 可见公平的代价是性能与吞吐量。 Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。 5.3 RedissonFairLock RedissonFairLock 用法依然很简单 RLock fairLock = redissonClient.getFairLock(lockName); fairLock.lock(); RedissonFairLock继承自RedissonLock,同样一路向下找到加锁实现方法tryLockInnerAsync。 这里有2段冗长的Lua,但是Debug发现,公平锁的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua较长,参数也多,我们着重分析Lua的实现规则 参数: -- lua中的几个参数 KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName) KEYS[1]: lock_name, 锁名称 KEYS[2]: "redisson_lock_queue:{xxx}" 线程队列 KEYS[3]: "redisson_lock_timeout:{xxx}" 线程id对应的超时集合 ARGV = internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime ARGV[1]: "{leaseTime}" 过期时间 ARGV[2]: "{Redisson.UUID}:{threadId}" ARGV[3] = 当前时间 + 线程等待时间:(10:00:00) + 5000毫秒 = 10:00:05 ARGV[4] = 当前时间(10:00:00) 部署服务器时间,非redis-server服务器时间 公平锁实现的Lua脚本 -- 1.死循环清除过期key while true do -- 获取头节点 local firstThreadId2 = redis.call('lindex', KEYS[2], 0); -- 首次获取必空跳出循环 if firstThreadId2 == false then break; end; -- 清除过期key local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2)); if timeout <= tonumber(ARGV[4]) then redis.call('zrem', KEYS[3], firstThreadId2); redis.call('lpop', KEYS[2]); else break; end; end; -- 2.不存在该锁 && (不存在线程等待队列 || 存在线程等待队列而且第一个节点就是此线程ID),加锁部分主要逻辑 if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then -- 弹出队列中线程id元素,删除Zset中该线程id对应的元素 redis.call('lpop', KEYS[2]); redis.call('zrem', KEYS[3], ARGV[2]); local keys = redis.call('zrange', KEYS[3], 0, -1); -- 遍历zSet所有key,将key的超时时间(score) - 当前时间ms for i = 1, #keys, 1 do redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]); end; -- 加锁设置锁过期时间 redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 3.线程存在,重入判断 if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then redis.call('hincrby', KEYS[1], ARGV[2],1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 4.返回当前线程剩余存活时间 local timeout = redis.call('zscore', KEYS[3], ARGV[2]); if timeout ~= false then -- 过期时间timeout的值在下方设置,此处的减法算出的依旧是当前线程的ttl return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]); end; -- 5.尾节点剩余存活时间 local lastThreadId = redis.call('lindex', KEYS[2], -1); local ttl; -- 尾节点不空 && 尾节点非当前线程 if lastThreadId ~= false and lastThreadId ~= ARGV[2] then -- 计算队尾节点剩余存活时间 ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]); else -- 获取lock_name剩余存活时间 ttl = redis.call('pttl', KEYS[1]); end; -- 6.末尾排队 -- zSet 超时时间(score),尾节点ttl + 当前时间 + 5000ms + 当前时间,无则新增,有则更新 -- 线程id放入队列尾部排队,无则插入,有则不再插入 local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then redis.call('rpush', KEYS[2], ARGV[2]); end; return ttl; 5.4 公平锁加锁步骤 通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。 其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤 1.队列清理 保证队列中只有未过期的等待线程 2.首次加锁 hset加锁,pexpire过期时间 3.重入判断 此处同可重入锁lua 4.返回ttl 5.计算尾节点ttl 初始值为锁的剩余过期时间 6.末尾排队 ttl + 2 * currentTime + waitTime是score的默认值计算公式 2.模拟 如果模拟以下顺序,就会明了redisson公平锁整个加锁流程 假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20 t1:当线程1初次获取锁 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束 t2:线程2尝试获取锁(线程1未释放锁) 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10 t3:线程3尝试获取锁(线程1未释放锁) 1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20 如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。 而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取) 10:00:30 线程2尝试获取锁(线程1已释放锁) 1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束 排队结构如图: 公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。 由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。 并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 6 总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。 Redisson也提供了多机情况下的联锁MultiLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock 和官方推荐的红锁RedLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(Java,redis,分布式,redis,redisson,java) Spring Boot:Java开发的神奇加速器(二) 小周不想卷 艾思科蓝学术会议投稿springboot 目录四、深入理解SpringBoot配置4.1配置文件类型4.2常用配置项4.3自定义配置五、数据访问与持久化5.1集成SpringDataJPA5.2编写数据访问层代码5.3事务管理四、深入理解SpringBoot配置4.1配置文件类型在SpringBoot应用中,主要有两种配置文件类型,即application.properties和application.yml(或application.y Jenkins 拉取 Git 分支代码问题排查与解决指南 码农阿豪@新空间 包罗万象疑难杂症解决方案git运维jenkins 个人名片作者简介:java领域优质创作者个人主页:码农阿豪工作室:新空间代码工作室(提供各种软件服务)个人邮箱:[2435024119@qq.com]个人微信:15279484656个人导航网站:www.forff.top座右铭:总有人要赢。为什么不能是我呢?专栏导航:码农阿豪系列专栏导航面试专栏:收集了java相关高频面试题,面试实战总结️Spring5系列专栏:整理了Spring5重要知识点与 如何发起http的请求,在系统中集成 红豆和绿豆 javahttp网络协议网络 在Java中发起HTTP请求,有多种开源框架可供选择。以下是一些常用的开源框架及其特点:1.ApacheHttpClientApacheHttpClient是一个功能强大的HTTP客户端库,支持同步和异步请求,广泛用于各种Java应用。2.OkHttpOkHttp是一个高效的HTTP客户端,支持HTTP/2和WebSocket,具有自动重试和恢复功能。3.UniRestUniRest是一个简单易用 Houdini:Houdini光照与渲染基础_2024-07-16_02-34-24.Tex chenjj4003 游戏开发houdiniandroidcinema4dblender游戏3dsmax Houdini:Houdini光照与渲染基础Houdini渲染引擎简介Mantra渲染器概述Mantra是Houdini自带的渲染引擎,它是一个基于物理的渲染器,能够处理复杂的光线追踪和全局光照效果。Mantra的设计理念是灵活性和可扩展性,它支持多种渲染模式,包括CPU渲染和GPU渲染,以及分布式渲染。Mantra的渲染质量高,特别适合于处理大规模的场景和复杂的视觉效果。Mantra渲染器的特点 创建全局异常处理器(Global Exception Processor) 易安杰 java开发语言jvmspringboot 先来了解一下什么是异常?在Java程序中,异常(Exception)是指在程序执行过程中发生的非正常情况,它打断了正常的指令流;Java中的异常处理是一种用于处理程序中错误和异常情况的一种机制。如上图所示异常主要分为两类,包括Error和Exception,两种异常有一个共同的父类是Throwable;在Exception中又分为RuntimeException(运行时异常)和CheckedExc 【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂! AllowM 算法hot100leetcodejava算法 [LeetCodeHot100]除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!✏️本文对应题目链接:除自身以外数组的乘积题目描述给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目数据保证数组之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。请不要使用除法,且在O(n)时间复杂度内完成 AGP 8.0 适配 ---- jvm target compatibility zhuzhumouse androidjavagradle 基础知识科普Android工程的GradleJDK,以及代码里面配置的jvmTarget有什么区别?这是两个完全不同的概念:GradleJDK:指的是用于运行Gradle构建系统的JavaDevelopmentKit版本。Gradle是Android项目的构建工具,它负责处理项目的编译、打包、依赖管理等任务。GradleJDK的选择决定了在构建过程中使用的Java版本。jvmTarget:是jav Java取绝对值 web15085181368 java 在Java中可以使用Math.abs()方法来方便的进行绝对值计算,例如:intvalue=Math.abs(-90);System.out.println(value);//90publicstaticintabs(inta){return(a<0)?-a:a;}当输入的是正数的时候直接返回即可,当是负数的时候返回它的相反数即可。使用三目运算符可以使用一行代码就能做到。如果需要输入Double或 RxJava 和Kotlin协程(Coroutines) Marblog JavaAndroidrxjavakotlinandroid RxJava和协程(Coroutines)都是处理异步编程和并发任务的强大工具,但它们的设计理念、使用方式和应用场景有所不同。以下是它们之间的主要区别:1.设计理念与核心概念RxJava:基于响应式流:RxJava是基于反应式编程(ReactiveProgramming)理念的库,它主要用于处理异步数据流和事件流。RxJava提供了丰富的操作符来对数据流进行组合、变换、过滤、错误处理等操作。数据流 强势破局:基于Java的开源能源管理系统源码+能碳管理系统+能源管理系统+能源管理平台:智碳能源管理系统,我为地球降一度!开启智慧节能新纪元,让能源管理“碳”索未来!可在线体验 智碳未来科技有限公司 能源开源javaspringboot物联网 先上干货!墙内仓库地址(码云):https://gitee.com/ustcyc/zhitan-ems已同步更新到github仓库:https://github.com/Andy-Yin/zhitan-ems在当今社会,能源管理已经成为一个不可忽视的重要议题。随着科技的不断发展,人们对能源的需求也日益增长,而能源的消耗对地球环境造成的影响也日益显现。为了更好地管理和利用能源资源,推动绿色可持续发展 Redis在实际应用中的最佳实践:缓存加速、分布式锁与消息队列 一碗黄焖鸡三碗米饭 Redis技术全景解析redisjava后端架构微服务 Redis在实际应用中的最佳实践:缓存加速、分布式锁与消息队列Redis作为一个高性能的内存数据库,凭借其高吞吐量、低延迟的特性,已成为开发者在构建现代应用时的首选技术之一。无论是在缓存加速、分布式锁,还是消息队列等多个应用场景中,Redis都展现出了卓越的性能。本文将围绕这三个核心场景,深入探讨Redis的最佳实践,帮助开发者更好地理解和应用Redis,在生产环境中提高系统的响应速度、稳定性与可 Redis基础笔记 JustGopher redis笔记java 一、基础知识连接方式CLI(CommandLineInterface)API(ApplicationProgrammingInterface)GUI(GraphicalUserInterface)启动redis-server连接到Redis(RedisCLIClient)redisredis-clitelnet127.0.0.16379退出quit/exit查看过期时间TTLkey设置过期时间ex Ubuntu 在/etc/profile中配置环境变量后,开启新终端窗口环境变量失效问题解决(非root用户) 深度视觉机器 Ubuntu20环境变量失效问题到此解决。 最近在使用ubuntu系统的过程中用非root用户登录,发现在安装完jdk并添加相关环境变量,java-version可以正常显示,但当我开启一个新的终端的时候,这个命令就失效了,网上查了一下原因,是需要修改当前登录用户的.bashrc文件中添加环境变量才可永久生效,下面来说明几种解决方案。1、临时解决方案(非root用户)临时解决方案就是重新加载/etc/profile文件,执行以下命令:$so 【系列专栏】银行IT的云原生架构-云单元架构 12 呱牛do it 金融科技云原生架构金融 银行IT的云原生架构-云单元架构一、引言在银行数字化转型进程中,云原生架构已成为提升竞争力、实现高效创新的关键支撑。其中,云单元架构作为一种先进的架构模式,正逐渐受到银行的关注与应用。云单元架构通过将复杂的系统拆分为多个相对独立、自治的单元,为银行带来了更高的灵活性、扩展性与可靠性。从目标、特征、单元化流量路由、应用与数据单元化、分布式中间件等多个关键角度深入剖析云单元架构,对于银行更好地理解和应 Mac Java 使用 tesseract 进行 ORC 识别 nukix macosjavamacosjava开发语言ORC 在Java开发中使用图片转文字时,难免会遇到问题,比如我使用Mac(M1芯片)系统进行开发,就出现报错。博主博客https://blog.uso6.comhttps://blog.csdn.net/dxk539687357一、直接使用1.使用brew进行安装brewinstalltesseract如果是其他系统的,建议看官方文档进行安装。2.查看版本nukix@nukixPC~%tesseract 音频采集(VUE3+JAVA) a26637896 音视频javajavascript vue部分代码xx.vueimportRecorderfrom'./Recorder.js';exportdefault{data(){return{mediaStream:null,recorder:null,isRecording:false,audioChunks:[],vadInterval:null//新增:用于存储声音活动检测的间隔ID};},asyncmounted(){this.m 渗透测试工具包 开源安全测试工具 网络安全工具_网络安全渗透测试工具(1) 2401_84254011 程序员安全开源测试工具 hackUtils-它是一个用于渗透测试和网络安全研究的黑客工具包,渗透以及web攻击脚本。msf框架:pocsscan攻击框架Pocsuite攻击框架Beebeeto攻击框架漏洞POC&EXP-ExploitDB官方git版本。php漏洞代码分析ysoserial-JAVA反序列化POC生成工具:JavaUnserializeExploits-JAVA反序列化EXP。JenkinsCommonC Java基础专项复习7——事务 Ttang23 java开发语言 Java基础专项复习系列目录1、Java基础专项复习1——函数关键字static-CSDN博客2、Java基础专项复习2——集合-CSDN博客3、Java基础专项复习3——Map集合-CSDN博客4、Java基础专项复习4——IO流-CSDN博客5、Java基础专项复习5——异常-CSDN博客6、Java基础专项复习6——多线程-CSDN博客文章目录目录Java基础专项复习系列目录文章目录1.1事 Android Java创建ViewModel新api debug_cat Android应用层开发androidjavaleetcode 背景项目使用Java,创建ViewModel发现之前旧api不管用了。不要问为什么项目还要用Java,别问。老项目不让升级。ViewModel创建新方式新方式是因为依赖新版本库,其实用旧版本库就回到旧方式了。依赖:deflifecycle_version="2.5.0"//ViewModelimplementation"androidx.lifecycle:lifecycle-viewmodel: 【Spring】Spring的模块架构与生态圈—Spring Boot、Spring Cloud与Spring Security AI人H哥会Java JAVAjavaspring后端开发语言springbootspringcloud 随着互联网的发展,企业对快速开发和高可用性的需求不断增加,Spring生态系统(包括SpringBoot、SpringCloud和SpringSecurity)应运而生,为Java开发提供了强大的支持。在实际应用中,SpringBoot使得开发者能够快速构建独立的、生产级的Spring应用;SpringCloud则为微服务架构提供了完整的解决方案;而SpringSecurity则为应用提供了安全保 Linux、Docker、Redis常见面试题 百百味 linuxdockerredis 1.Linux什么是Linux?Linux是一种基于UNIX的操作系统,最初是由LinusTorvalds引入的。它基于Linux内核,可以运行在由Intel,MIPS,HP,IBM,SPARC和Motorola制造的不同硬件平台上。Linux中另一个受欢迎的元素是它的吉祥物,一个名叫Tux的企鹅形象。UNIX和LINUX有什么区别?Unix最初是作为BellLaboratories的专有操作系统 Python学习心得体会 yuetouwen pythonwindows开发语言 一、引言Python作为一种高级编程语言,以其简洁性、易读性和强大的功能在当今的编程领域中占据着重要地位。在学习Python的过程中,我不仅掌握了一种新的编程工具,更深入地理解了编程的思维方式和逻辑结构。二、语法基础与编程环境搭建Python的语法简洁明了,相较于其他编程语言,其代码更接近自然语言。例如,使用缩进来表示代码块,而不是像C或Java那样使用大括号。在学习初期,我快速掌握了变量的定义、 【Java】—— 包装类&泛型 两袖清风998 数据结构与算法java开发语言 一、包装类(WrapperClass)1、包装类的定义在Java中数据分为两大类:基础数据类型(内置数据类型)引用类型其中基础数据类型(byteintlongdoublebooleanchar......)和C语言接近是早期为了吸引C语言的程序员。后续Java有引入了一系列更进阶的语法机制,而这些机制又依赖于引用类型。Java希望把所有的类型统一成Object体系,让基础数据类型也可以使用equa JSP(学习自用) 文城521 JAVA实训java学习html前端 一、本质JSP解析后就是Servlet类的java代码。二、jsp内嵌java代码1、声明脚本用于声明属性和方法。2、运行脚本相当于在service方法中写代码3、打印脚本用于打印上面两个脚本内声明的变量结果啥的。//页面会显示张三三、jsp内置对象1、request代表客户端的请求。2、response代表服务端的响应。3、session代表客户端当前会话。4、application代表整个We Effective Java学习笔记 lucky。 Java学习java 静态工厂方法考虑使用静态工厂方法代替构造静态工厂方法与构造器不同的第一优势在于,它们有名字第二个优势,不用每次被调用时都创建新对象第三个优势,可以返回原返回类型的子类第四个优势,在创建带泛型的实例时,能使代码变得简洁(jdk1.8已经解决)除此之外可以有多个参数相同但名称不同的工厂方法可以减少对外暴露的属性多了一层控制,方便统一修改Java中,获得一个类实例最简单的方法就是使用new关键字,通过构 java 代码走查_代码走查如何保证软件质量 weixin_40006965 java代码走查 目的代码走查的好处非常多,第一个是让新同学快速熟悉代码并了解系统。第二个是做资损防控的事前检查,在事前规避引发线上故障。第三个是通过一起讨论和审查,加强团队代码阅读和编写能力,让大家编写出优秀的代码。代码走查的优点非常多,但是最核心的还是希望通过代码走查提前发现问题并解决问题。所以基于以上目的,代码走查不是为了找到代码写的差的程序员加以批评,不是为了找到差的代码,而是一起发现问题共同成长,所以对于 mongoDB分片集群部署 glnullops 运维mongodb数据库nosql 一、MongoDB背景MongoDB是一款功能完善的分布式文档数据库,是一款非常出名的NoSQL数据库。当前国内使用Mongodb的大型实践越来越多,MongoDB为我司提供了重要的数据库存储服务,支撑着每天近千万级QPS峰值读写,数万亿级数据量存储服务。MongoDB在高性能、动态扩缩容、高可用、易部署、易使用、海量数据存储等方面拥有很大优势。近些年,MongoDB在DB-Engines流行度排 【Java基础-47.1】Java中通过继承Thread类创建线程 AllenBright #Java基础java开发语言 在Java中,多线程编程是实现并发操作的重要手段之一。Java提供了多种创建线程的方式,其中一种是通过继承Thread类来创建线程。本文将详细介绍如何通过继承Thread类创建线程,并探讨其使用场景、优缺点以及注意事项。1.什么是Thread类?Thread类是Java中用于表示线程的核心类。它位于java.lang包中,提供了线程的创建、启动、暂停、中断等操作。每个Thread对象都代表一个独立 java代码走查_java代码开发完成后,代码走查规范 游凯超 java代码走查 代码走查注意事项:1、不变的值,尽量写个常量类2、尽量使用if{}else,不要一直if去判断3、减少循环调用方法查询数据库4、dao层尽量不要用逻辑,尽量在service里写业务逻辑5、金额使用Bigdecimal类型的,0.00这种格式靠右显示6、iframe的弹框,要放到js里,可以缓存,放到jsp里每次都需要加载。7、ajax对应success对应一个error异常,尽量用error,网络 java 代码走查_java代码走查计划书 沙鸥123 java代码走查 《java代码走查计划书》由会员分享,可在线阅读,更多相关《java代码走查计划书(8页珍藏版)》请在人人文库网上搜索。1、WATERCorporation代码走查计划书Version2.0XXX2012/3/20文档修改记录版本号主要作者修改记录完成日期1.0无2010-03-181.1待评审物2010-03-181.2评审流程2010-03-192.0人员分工、评审流程2010-03-20目录 PHP,安卓,UI,java,linux视频教程合集 cocos2d-x小菜 javaUIlinuxPHPandroid ╔-----------------------------------╗┆ zookeeper admin 笔记 braveCS zookeeper Required Software 1) JDK>=1.6 2)推荐使用ensemble的ZooKeeper(至少3台),并run on separate machines 3)在Yahoo!,zk配置在特定的RHEL boxes里,2个cpu,2G内存,80G硬盘 数据和日志目录 1)数据目录里的文件是zk节点的持久化备份,包括快照和事务日 Spring配置多个连接池 easterfly spring 项目中需要同时连接多个数据库的时候,如何才能在需要用到哪个数据库就连接哪个数据库呢? Spring中有关于dataSource的配置: <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" &nb Mysql 171815164 mysql 例如,你想myuser使用mypassword从任何主机连接到mysql服务器的话。 GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%'IDENTIFIED BY 'mypassword' WI TH GRANT OPTION; 如果你想允许用户myuser从ip为192.168.1.6的主机连接到mysql服务器,并使用mypassword作 CommonDAO(公共/基础DAO) g21121 DAO 好久没有更新博客了,最近一段时间工作比较忙,所以请见谅,无论你是爱看呢还是爱看呢还是爱看呢,总之或许对你有些帮助。 DAO(Data Access Object)是一个数据访问(顾名思义就是与数据库打交道)接口,DAO一般在业 直言有讳 永夜-极光 感悟随笔 1.转载地址:http://blog.csdn.net/jasonblog/article/details/10813313 精华: “直言有讳”是阿里巴巴提倡的一种观念,而我在此之前并没有很深刻的认识。为什么呢?就好比是读书时候做阅读理解,我喜欢我自己的解读,并不喜欢老师给的意思。在这里也是。我自己坚持的原则是互相尊重,我觉得阿里巴巴很多价值观其实是基本的做人 安装CentOS 7 和Win 7后,Win7 引导丢失 随便小屋 centos 一般安装双系统的顺序是先装Win7,然后在安装CentOS,这样CentOS可以引导WIN 7启动。但安装CentOS7后,却找不到Win7 的引导,稍微修改一点东西即可。 一、首先具有root 的权限。 即进入Terminal后输入命令su,然后输入密码即可 二、利用vim编辑器打开/boot/grub2/grub.cfg文件进行修改 v Oracle备份与恢复案例 aijuans oracle Oracle备份与恢复案例 一. 理解什么是数据库恢复当我们使用一个数据库时,总希望数据库的内容是可靠的、正确的,但由于计算机系统的故障(硬件故障、软件故障、网络故障、进程故障和系统故障)影响数据库系统的操作,影响数据库中数据的正确性,甚至破坏数据库,使数据库中全部或部分数据丢失。因此当发生上述故障后,希望能重构这个完整的数据库,该处理称为数据库恢复。恢复过程大致可以分为复原(Restore)与 JavaEE开源快速开发平台G4Studio v5.0发布 無為子 我非常高兴地宣布,今天我们最新的JavaEE开源快速开发平台G4Studio_V5.0版本已经正式发布。 访问G4Studio网站 http://www.g4it.org 2013-04-06 发布G4Studio_V5.0版本 功能新增 (1). 新增了调用Oracle存储过程返回游标,并将游标映射为Java List集合对象的标 Oracle显示根据高考分数模拟录取 百合不是茶 PL/SQL编程oracle例子模拟高考录取学习交流 题目要求: 1,创建student表和result表 2,pl/sql对学生的成绩数据进行处理 3,处理的逻辑是根据每门专业课的最低分线和总分的最低分数线自动的将录取和落选 1,创建student表,和result表 学生信息表; create table student( student_id number primary key,--学生id 优秀的领导与差劲的领导 bijian1013 领导管理团队 责任 优秀的领导:优秀的领导总是对他所负责的项目担负起责任。如果项目不幸失败了,那么他知道该受责备的人是他自己,并且敢于承认错误。 差劲的领导:差劲的领导觉得这不是他的问题,因此他会想方设法证明是他的团队不行,或是将责任归咎于团队中他不喜欢的那几个成员身上。 努力工作 优秀的领导:团队领导应该是团队成员的榜样。至少,他应该与团队中的其他成员一样努力工作。这仅仅因为他 js函数在浏览器下的兼容 Bill_chen jquery浏览器IEDWRext 做前端开发的工程师,少不了要用FF进行测试,纯js函数在不同浏览器下,名称也可能不同。对于IE6和FF,取得下一结点的函数就不尽相同: IE6:node.nextSibling,对于FF是不能识别的; FF:node.nextElementSibling,对于IE是不能识别的; 兼容解决方式:var Div = node.nextSibl 【JVM四】老年代垃圾回收:吞吐量垃圾收集器(Throughput GC) bit1129 垃圾回收 吞吐量与用户线程暂停时间 衡量垃圾回收算法优劣的指标有两个: 吞吐量越高,则算法越好 暂停时间越短,则算法越好 首先说明吞吐量和暂停时间的含义。 垃圾回收时,JVM会启动几个特定的GC线程来完成垃圾回收的任务,这些GC线程与应用的用户线程产生竞争关系,共同竞争处理器资源以及CPU的执行时间。GC线程不会对用户带来的任何价值,因此,好的GC应该占 J2EE监听器和过滤器基础 白糖_ J2EE Servlet程序由Servlet,Filter和Listener组成,其中监听器用来监听Servlet容器上下文。 监听器通常分三类:基于Servlet上下文的ServletContex监听,基于会话的HttpSession监听和基于请求的ServletRequest监听。 ServletContex监听器 ServletContex又叫application 博弈AngularJS讲义(16) - 提供者 boyitech jsAngularJSapiAngularProvider Angular框架提供了强大的依赖注入机制,这一切都是有注入器(injector)完成. 注入器会自动实例化服务组件和符合Angular API规则的特殊对象,例如控制器,指令,过滤器动画等。 那注入器怎么知道如何去创建这些特殊的对象呢? Angular提供了5种方式让注入器创建对象,其中最基础的方式就是提供者(provider), 其余四种方式(Value, Fac java-写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 bylijinnan java public class CommonSubSequence { /** * 题目:写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 * 写一个版本算法复杂度O(N^2)和一个O(N) 。 * * O(N^2):对于a中的每个字符,遍历b中的每个字符,如果相同,则拷贝到新字符串中。 * O( sqlserver 2000 无法验证产品密钥 Chen.H sqlwindowsSQL ServerMicrosoft 在 Service Pack 4 (SP 4), 是运行 Microsoft Windows Server 2003、 Microsoft Windows Storage Server 2003 或 Microsoft Windows 2000 服务器上您尝试安装 Microsoft SQL Server 2000 通过卷许可协议 (VLA) 媒体。 这样做, 收到以下错误信息CD KEY的 SQ [新概念武器]气象战争 comsci 气象战争的发动者必须是拥有发射深空航天器能力的国家或者组织.... 原因如下: 地球上的气候变化和大气层中的云层涡旋场有密切的关系,而维持一个在大气层某个层次 oracle 中 rollup、cube、grouping 使用详解 daizj oraclegroupingrollupcube oracle 中 rollup、cube、grouping 使用详解 -- 使用oracle 样例表演示 转自namesliu -- 使用oracle 的样列库,演示 rollup, cube, grouping 的用法与使用场景 --- ROLLUP , 为了理解分组的成员数量,我增加了 分组的计数 COUNT(SAL) 技术资料汇总分享 Dead_knight 技术资料汇总 分享 本人汇总的技术资料,分享出来,希望对大家有用。 http://pan.baidu.com/s/1jGr56uE 资料主要包含: Workflow->工作流相关理论、框架(OSWorkflow、JBPM、Activiti、fireflow...) Security->java安全相关资料(SSL、SSO、SpringSecurity、Shiro、JAAS...) Ser 初一下学期难记忆单词背诵第一课 dcj3sjt126com englishword could 能够 minute 分钟 Tuesday 星期二 February 二月 eighteenth 第十八 listen 听 careful 小心的,仔细的 short 短的 heavy 重的 empty 空的 certainly 当然 carry 携带;搬运 tape 磁带 basket 蓝子 bottle 瓶 juice 汁,果汁 head 头;头部 截取视图的图片, 然后分享出去 dcj3sjt126com OSObjective-C OS 7 has a new method that allows you to draw a view hierarchy into the current graphics context. This can be used to get an UIImage very fast. I implemented a category method on UIView to get the vi MySql重置密码 fanxiaolong MySql重置密码 方法一: 在my.ini的[mysqld]字段加入: skip-grant-tables 重启mysql服务,这时的mysql不需要密码即可登录数据库 然后进入mysql mysql>use mysql; mysql>更新 user set password=password('新密码') WHERE User='root'; mysq Ehcache(03)——Ehcache中储存缓存的方式 234390216 ehcacheMemoryStoreDiskStore存储驱除策略 Ehcache中储存缓存的方式 目录 1 堆内存(MemoryStore) 1.1 指定可用内存 1.2 驱除策略 1.3 元素过期 2 &nbs spring mvc中的@propertysource jackyrong spring mvc 在spring mvc中,在配置文件中的东西,可以在java代码中通过注解进行读取了: @PropertySource 在spring 3.1中开始引入 比如有配置文件 config.properties mongodb.url=1.2.3.4 mongodb.db=hello 则代码中 @PropertySource(& 重学单例模式 lanqiu17 单例Singleton模式 最近在重新学习设计模式,感觉对模式理解更加深刻。觉得有必要记下来。 第一个学的就是单例模式,单例模式估计是最好理解的模式了。它的作用就是防止外部创建实例,保证只有一个实例。 单例模式的常用实现方式有两种,就人们熟知的饱汉式与饥汉式,具体就不多说了。这里说下其他的实现方式 静态内部类方式: package test.pattern.singleton.statics; publ .NET开源核心运行时,且行且珍惜 netcome java.net开源 背景 2014年11月12日,ASP.NET之父、微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣布,微软将开源全部.NET核心运行时,并将.NET 扩展为可在 Linux 和 Mac OS 平台上运行。.NET核心运行时将基于MIT开源许可协议发布,其中将包括执行.NET代码所需的一切项目——CLR、JIT编译器、垃圾收集器(GC)和核心 使用oscahe缓存技术减少与数据库的频繁交互 Everyday都不同 Web高并发oscahe缓存 此前一直不知道缓存的具体实现,只知道是把数据存储在内存中,以便下次直接从内存中读取。对于缓存的使用也没有概念,觉得缓存技术是一个比较”神秘陌生“的领域。但最近要用到缓存技术,发现还是很有必要一探究竟的。 缓存技术使用背景:一般来说,对于web项目,如果我们要什么数据直接jdbc查库好了,但是在遇到高并发的情形下,不可能每一次都是去查数据库,因为这样在高并发的情形下显得不太合理—— Spring+Mybatis 手动控制事务 toknowme mybatis @Override public boolean testDelete(String jobCode) throws Exception { boolean flag = false; &nbs 菜鸟级的android程序员面试时候需要掌握的知识点 xp9802 android 熟悉Android开发架构和API调用 掌握APP适应不同型号手机屏幕开发技巧 熟悉Android下的数据存储 熟练Android Debug Bridge Tool 熟练Eclipse/ADT及相关工具 熟悉Android框架原理及Activity生命周期 熟练进行Android UI布局 熟练使用SQLite数据库; 熟悉Android下网络通信机制,S 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
2.0似乎更像一把锁,但好像又缺少了什么,小张一拍脑袋,synchronized和ReentrantLock都很丝滑,因为他们都是可重入锁,一个线程多次拿锁也不会死锁,我们需要可重入。
重入就是,同一个线程多次获取同一把锁是允许的,不会造成死锁,这一点synchronized偏向锁提供了很好的思路,synchronized的实现重入是在JVM层面,JAVA对象头MARK WORD中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。
再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放
仿造该方案,我们需改造Lua脚本:
1.需要存储 锁名称lockName、获得该锁的线程id和对应线程的进入次数count
2.加锁
每次线程获取锁时,判断是否已存在该锁
不存在
设置hash的key为线程id,value初始化为1
设置过期时间
返回获取锁成功true
存在
继续判断是否存在当前线程id的hash key
存在,线程key的value + 1,重入次数增加1,设置过期时间
不存在,返回加锁失败
3.解锁
每次线程来解锁时,判断是否已存在该锁
是否有该线程的id的hash key,有则减1,无则返回解锁失败
减1后,判断剩余count是否为0,为0则说明不再需要这把锁,执行del命令删除
为了方便维护这个对象,我们用Hash结构来存储这些字段。Redis的Hash类似Java的HashMap,适合存储对象。
hset lockname1 threadId 1
设置一个名字为lockname1的hash结构,该hash结构key为threadId,值value为1
hget lockname1 threadId
获取lockname1的threadId的值
存储结构为:
lockname 锁名称 key1: threadId 唯一键,线程id value1: count 计数器,记录该线程获取锁的次数
redis中的结构
当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减
判断一个redis key是否存在,可以用exists,而判断一个hash的key是否存在,可以用hexists
而redis也有hash自增的命令hincrby
每次自增1时 hincrby lockname1 threadId 1,自减1时 hincrby lockname1 threadId -1
当一把锁不再被需要了,每次解锁一次,count减1,直到为0时,执行删除
综合上述的存储结构和判断流程,加锁和解锁Lua如下
加锁 lock.lua:
local key = KEYS[1]; local threadId = ARGV[1]; local releaseTime = ARGV[2]; -- lockname不存在 if(redis.call('exists', key) == 0) then redis.call('hset', key, threadId, '1'); redis.call('expire', key, releaseTime); return 1; end; -- 当前线程已id存在 if(redis.call('hexists', key, threadId) == 1) then redis.call('hincrby', key, threadId, '1'); redis.call('expire', key, releaseTime); return 1; end; return 0;
解锁 unlock.lua:
local key = KEYS[1]; local threadId = ARGV[1]; -- lockname、threadId不存在 if (redis.call('hexists', key, threadId) == 0) then return nil; end; -- 计数器-1 local count = redis.call('hincrby', key, threadId, -1); -- 删除lock if (count == 0) then redis.call('del', key); return nil; end;
代码:
/** * @description 原生redis实现分布式锁 **/ @Getter @Setter public class RedisLock { private RedisTemplate redisTemplate; private DefaultRedisScript lockScript; private DefaultRedisScript unlockScript; public RedisLock(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; // 加载加锁的脚本 lockScript = new DefaultRedisScript<>(); this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua"))); this.lockScript.setResultType(Long.class); // 加载释放锁的脚本 unlockScript = new DefaultRedisScript<>(); this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua"))); } /** * 获取锁 */ public String tryLock(String lockName, long releaseTime) { // 存入的线程信息的前缀 String key = UUID.randomUUID().toString(); // 执行脚本 Long result = (Long) redisTemplate.execute( lockScript, Collections.singletonList(lockName), key + Thread.currentThread().getId(), releaseTime); if (result != null && result.intValue() == 1) { return key; } else { return null; } } /** * 解锁 * @param lockName * @param key */ public void unlock(String lockName, String key) { redisTemplate.execute(unlockScript, Collections.singletonList(lockName), key + Thread.currentThread().getId() ); } } 至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。 严谨的小张觉得虽然当个普通互斥锁,已经稳稳够用,可是业务里总是又很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题。 而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。 小张不是杠精,因为库存操作总有这样那样的特殊。 所以我们希望在这种情况时,可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果,这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约。 读写分离也是常见,一个读多写少的业务为了性能,常常是有读锁和写锁的。 而此刻的扩展已经超出了一把简单轮子的复杂程度,光是处理续约,就够小张喝一壶,何况在性能(锁的最大等待时间)、优雅(无效锁申请)、重试(失败重试机制)等方面还要下功夫研究。 在小张苦思冥想时,旁边的小白凑过来看了看小张,很好奇,都2021年了,为什么不直接用redisson呢? Redisson就有这把你要的锁。 3 Redisson分布式锁 号称简单的Redisson分布式锁的使用姿势是什么? 3.1 依赖 org.redisson redisson 3.13.6 org.redisson redisson-spring-boot-starter 3.13.6 3.2 配置 @Configuration public class RedissionConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.password}") private String password; private int port = 6379; @Bean public RedissonClient getRedisson() { Config config = new Config(); config.useSingleServer(). setAddress("redis://" + redisHost + ":" + port). setPassword(password); config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); } } 3.3 启用分布式锁 @Resource private RedissonClient redissonClient; RLock rLock = redissonClient.getLock(lockName); try { boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS); if (isLocked) { // TODO } } catch (Exception e) { rLock.unlock(); } 简洁明了,只需要一个RLock,既然推荐Redisson,就往里面看看他是怎么实现的。 4 RLock RLock是Redisson分布式锁的最核心接口,继承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson执行异步实现的核心逻辑,也是Netty发挥的主要阵地。 RLock如何加锁? 从RLock进入,找到RedissonLock类,找到tryLock方法再递进到干事的tryAcquireOnceAsync方法,这是加锁的主要代码(版本不一此处实现有差别,和最新3.15.x有一定出入,但是核心逻辑依然未变。此处以3.13.6为例) private RFuture tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else { RFuture ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } } 此处出现leaseTime时间判断的2个分支,实际上就是加锁时是否设置过期时间,未设置过期时间(-1)时则会有watchDog的锁续约(下文),一个注册了加锁事件的续约任务。我们先来看有过期时间tryLockInnerAsync部分, evalWriteAsync是eval命令执行lua的入口 RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); } 这里揭开真面目,eval命令执行Lua脚本的地方,此处的Lua脚本展开 -- 不存在该key时 if (redis.call('exists', KEYS[1]) == 0) then -- 新增该锁并且hash中该线程id对应的count置1 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间 redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 存在该key 并且 hash中线程id的key也存在 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 线程重入次数++ redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]); 和前面我们写自定义的分布式锁的脚本几乎一致,看来redisson也是一样的实现,具体参数分析: // keyName KEYS[1] = Collections.singletonList(this.getName()) // leaseTime ARGV[1] = this.internalLockLeaseTime // uuid+threadId组合的唯一值 ARGV[2] = this.getLockName(threadId) 总共3个参数完成了一段逻辑: 判断该锁是否已经有对应hash表存在, • 没有对应的hash表:则set该hash表中一个entry的key为锁名称,value为1,之后设置该hash表失效时间为leaseTime • 存在对应的hash表:则将该lockName的value执行+1操作,也就是计算进入次数,再设置失效时间leaseTime • 最后返回这把锁的ttl剩余时间 也和上述自定义锁没有区别 既然如此,那解锁的步骤也肯定有对应的-1操作,再看unlock方法,同样查找方法名,一路到 protected RFuture unlockInnerAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)}); } 掏出Lua部分 -- 不存在key if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; -- 计数器 -1 local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then -- 过期时间重设 redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else -- 删除并发布解锁消息 redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil; 该Lua KEYS有2个Arrays.asList(getName(), getChannelName()) name 锁名称 channelName,用于pubSub发布消息的channel名称 ARGV变量有三个LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId) LockPubSub.UNLOCK_MESSAGE,channel发送消息的类别,此处解锁为0 internalLockLeaseTime,watchDog配置的超时时间,默认为30s lockName 这里的lockName指的是uuid和threadId组合的唯一值 步骤如下: 1.如果该锁不存在则返回nil; 2.如果该锁存在则将其线程的hash key计数器-1, 3.计数器counter>0,重置下失效时间,返回0;否则,删除该锁,发布解锁消息unlockMessage,返回1; 其中unLock的时候使用到了Redis发布订阅PubSub完成消息通知。 而订阅的步骤就在RedissonLock的加锁入口的lock方法里 long threadId = Thread.currentThread().getId(); Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId); if (ttl != null) { // 订阅 RFuture future = this.subscribe(threadId); if (interruptibly) { this.commandExecutor.syncSubscriptionInterrupted(future); } else { this.commandExecutor.syncSubscription(future); } // 省略 当锁被其他线程占用时,通过监听锁的释放通知(在其他线程通过RedissonLock释放锁时,会通过发布订阅pub/sub功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。 4.1 解锁消息 为了一探究竟通知了什么,通知后又做了什么,进入LockPubSub。 这里只有一个明显的监听方法onMessage,其订阅和信号量的释放都在父类PublishSubscribe,我们只关注监听事件的实际操作 protected void onMessage(RedissonLockEntry value, Long message) { Runnable runnableToExecute; if (message.equals(unlockMessage)) { // 从监听器队列取监听线程执行监听回调 runnableToExecute = (Runnable)value.getListeners().poll(); if (runnableToExecute != null) { runnableToExecute.run(); } // getLatch()返回的是Semaphore,信号量,此处是释放信号量 // 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁 value.getLatch().release(); } else if (message.equals(readUnlockMessage)) { while(true) { runnableToExecute = (Runnable)value.getListeners().poll(); if (runnableToExecute == null) { value.getLatch().release(value.getLatch().getQueueLength()); break; } runnableToExecute.run(); } } } 发现一个是默认解锁消息,一个是读锁解锁消息,因为redisson是有提供读写锁的,而读写锁读读情况和读写、写写情况互斥情况不同,我们只看上面的默认解锁消息unlockMessage分支 LockPubSub监听最终执行了2件事 runnableToExecute.run() 执行监听回调 value.getLatch().release(); 释放信号量 Redisson通过LockPubSub监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁。 这时再回来看tryAcquireOnceAsync另一分支 private RFuture tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else { RFuture ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } } 可以看到,无超时时间时,在执行加锁操作后,还执行了一段费解的逻辑 ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }) } } }) 此处涉及到Netty的Future/Promise-Listener模型,Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解 在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。 这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务。 这段定时任务的就是watchDog的核心。 4.2 锁续约 查看RedissonLock.this.scheduleExpirationRenewal(threadId) private void scheduleExpirationRenewal(long threadId) { RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry(); RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry); if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); this.renewExpiration(); } } private void renewExpiration() { RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName()); if (ee != null) { Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() { public void run(Timeout timeout) throws Exception { RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName()); if (ent != null) { Long threadId = ent.getFirstThreadId(); if (threadId != null) { RFuture future = RedissonLock.this.renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e); } else { if (res) { RedissonLock.this.renewExpiration(); } } }); } } } }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); ee.setTimeout(task); } } 拆分来看,这段连续嵌套且冗长的代码实际上做了几步 • 添加一个netty的Timeout回调任务,每(internalLockLeaseTime / 3)毫秒执行一次,执行的方法是renewExpirationAsync • renewExpirationAsync重置了锁超时时间,又注册一个监听器,监听回调又执行了renewExpiration renewExpirationAsync 的Lua如下 protected RFuture renewExpirationAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); } if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0; 重新设置了超时时间。 Redisson加这段逻辑的目的是什么? 目的是为了某种场景下保证业务不影响,如任务执行超时但未结束,锁已经释放的问题。 当一个线程持有了一把锁,由于并未设置超时时间leaseTime,Redisson默认配置了30S,开启watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁。 这就是Redisson的锁续约,也就是WatchDog实现的基本思路。 4.3 流程概括 通过整体的介绍,流程简单概括: A、B线程争抢一把锁,A获取到后,B阻塞 B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息 A操作完成释放了锁,B线程收到订阅消息通知 B被唤醒开始继续抢锁,拿到锁 详细加锁解锁流程总结如下图: 5 公平锁 以上介绍的可重入锁是非公平锁,Redisson还基于Redis的队列(List)和ZSet实现了公平 5.1 公平的定义是什么? 公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少 5.2 FairSync 回顾JUC的ReentrantLock公平锁的实现 /** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } } AQS已经提供了整个实现,是否公平取决于实现类取出节点逻辑是否顺序取 AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,通过内置FIFO队列来完成资源获取线程的排队工作,他自身没有实现同步接口,仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用(上图),支持独占和共享获取,这是基于模版方法模式的一种设计,给公平/非公平提供了土壤。 我们用2张图来简单解释AQS的等待流程(出自《JAVA并发编程的艺术》) 一张是同步队列(FIFO双向队列)管理 获取同步状态失败(抢锁失败)的线程引用、等待状态和前驱后继节点的流程图 一张是独占式获取同步状态的总流程,核心acquire(int arg)方法调用流程 可以看出锁的获取流程 AQS维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋,移出队列或停止自旋的条件是前驱节点为头节点切成功获取了同步状态。 而比较另一段非公平锁类NonfairSync可以发现,控制公平和非公平的关键代码,在于hasQueuedPredecessors方法。 static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } NonfairSync减少了了hasQueuedPredecessors判断条件,该方法的作用就是 查看同步队列中当前节点是否有前驱节点,如果有比当前线程更早请求获取锁则返回true。 保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则 5.2.1 为什么JUC以默认非公平锁呢? 因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。 可见公平的代价是性能与吞吐量。 Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。 5.3 RedissonFairLock RedissonFairLock 用法依然很简单 RLock fairLock = redissonClient.getFairLock(lockName); fairLock.lock(); RedissonFairLock继承自RedissonLock,同样一路向下找到加锁实现方法tryLockInnerAsync。 这里有2段冗长的Lua,但是Debug发现,公平锁的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua较长,参数也多,我们着重分析Lua的实现规则 参数: -- lua中的几个参数 KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName) KEYS[1]: lock_name, 锁名称 KEYS[2]: "redisson_lock_queue:{xxx}" 线程队列 KEYS[3]: "redisson_lock_timeout:{xxx}" 线程id对应的超时集合 ARGV = internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime ARGV[1]: "{leaseTime}" 过期时间 ARGV[2]: "{Redisson.UUID}:{threadId}" ARGV[3] = 当前时间 + 线程等待时间:(10:00:00) + 5000毫秒 = 10:00:05 ARGV[4] = 当前时间(10:00:00) 部署服务器时间,非redis-server服务器时间 公平锁实现的Lua脚本 -- 1.死循环清除过期key while true do -- 获取头节点 local firstThreadId2 = redis.call('lindex', KEYS[2], 0); -- 首次获取必空跳出循环 if firstThreadId2 == false then break; end; -- 清除过期key local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2)); if timeout <= tonumber(ARGV[4]) then redis.call('zrem', KEYS[3], firstThreadId2); redis.call('lpop', KEYS[2]); else break; end; end; -- 2.不存在该锁 && (不存在线程等待队列 || 存在线程等待队列而且第一个节点就是此线程ID),加锁部分主要逻辑 if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then -- 弹出队列中线程id元素,删除Zset中该线程id对应的元素 redis.call('lpop', KEYS[2]); redis.call('zrem', KEYS[3], ARGV[2]); local keys = redis.call('zrange', KEYS[3], 0, -1); -- 遍历zSet所有key,将key的超时时间(score) - 当前时间ms for i = 1, #keys, 1 do redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]); end; -- 加锁设置锁过期时间 redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 3.线程存在,重入判断 if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then redis.call('hincrby', KEYS[1], ARGV[2],1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 4.返回当前线程剩余存活时间 local timeout = redis.call('zscore', KEYS[3], ARGV[2]); if timeout ~= false then -- 过期时间timeout的值在下方设置,此处的减法算出的依旧是当前线程的ttl return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]); end; -- 5.尾节点剩余存活时间 local lastThreadId = redis.call('lindex', KEYS[2], -1); local ttl; -- 尾节点不空 && 尾节点非当前线程 if lastThreadId ~= false and lastThreadId ~= ARGV[2] then -- 计算队尾节点剩余存活时间 ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]); else -- 获取lock_name剩余存活时间 ttl = redis.call('pttl', KEYS[1]); end; -- 6.末尾排队 -- zSet 超时时间(score),尾节点ttl + 当前时间 + 5000ms + 当前时间,无则新增,有则更新 -- 线程id放入队列尾部排队,无则插入,有则不再插入 local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then redis.call('rpush', KEYS[2], ARGV[2]); end; return ttl; 5.4 公平锁加锁步骤 通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。 其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤 1.队列清理 保证队列中只有未过期的等待线程 2.首次加锁 hset加锁,pexpire过期时间 3.重入判断 此处同可重入锁lua 4.返回ttl 5.计算尾节点ttl 初始值为锁的剩余过期时间 6.末尾排队 ttl + 2 * currentTime + waitTime是score的默认值计算公式 2.模拟 如果模拟以下顺序,就会明了redisson公平锁整个加锁流程 假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20 t1:当线程1初次获取锁 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束 t2:线程2尝试获取锁(线程1未释放锁) 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10 t3:线程3尝试获取锁(线程1未释放锁) 1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20 如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。 而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取) 10:00:30 线程2尝试获取锁(线程1已释放锁) 1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束 排队结构如图: 公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。 由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。 并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 6 总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。 Redisson也提供了多机情况下的联锁MultiLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock 和官方推荐的红锁RedLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(Java,redis,分布式,redis,redisson,java) Spring Boot:Java开发的神奇加速器(二) 小周不想卷 艾思科蓝学术会议投稿springboot 目录四、深入理解SpringBoot配置4.1配置文件类型4.2常用配置项4.3自定义配置五、数据访问与持久化5.1集成SpringDataJPA5.2编写数据访问层代码5.3事务管理四、深入理解SpringBoot配置4.1配置文件类型在SpringBoot应用中,主要有两种配置文件类型,即application.properties和application.yml(或application.y Jenkins 拉取 Git 分支代码问题排查与解决指南 码农阿豪@新空间 包罗万象疑难杂症解决方案git运维jenkins 个人名片作者简介:java领域优质创作者个人主页:码农阿豪工作室:新空间代码工作室(提供各种软件服务)个人邮箱:[2435024119@qq.com]个人微信:15279484656个人导航网站:www.forff.top座右铭:总有人要赢。为什么不能是我呢?专栏导航:码农阿豪系列专栏导航面试专栏:收集了java相关高频面试题,面试实战总结️Spring5系列专栏:整理了Spring5重要知识点与 如何发起http的请求,在系统中集成 红豆和绿豆 javahttp网络协议网络 在Java中发起HTTP请求,有多种开源框架可供选择。以下是一些常用的开源框架及其特点:1.ApacheHttpClientApacheHttpClient是一个功能强大的HTTP客户端库,支持同步和异步请求,广泛用于各种Java应用。2.OkHttpOkHttp是一个高效的HTTP客户端,支持HTTP/2和WebSocket,具有自动重试和恢复功能。3.UniRestUniRest是一个简单易用 Houdini:Houdini光照与渲染基础_2024-07-16_02-34-24.Tex chenjj4003 游戏开发houdiniandroidcinema4dblender游戏3dsmax Houdini:Houdini光照与渲染基础Houdini渲染引擎简介Mantra渲染器概述Mantra是Houdini自带的渲染引擎,它是一个基于物理的渲染器,能够处理复杂的光线追踪和全局光照效果。Mantra的设计理念是灵活性和可扩展性,它支持多种渲染模式,包括CPU渲染和GPU渲染,以及分布式渲染。Mantra的渲染质量高,特别适合于处理大规模的场景和复杂的视觉效果。Mantra渲染器的特点 创建全局异常处理器(Global Exception Processor) 易安杰 java开发语言jvmspringboot 先来了解一下什么是异常?在Java程序中,异常(Exception)是指在程序执行过程中发生的非正常情况,它打断了正常的指令流;Java中的异常处理是一种用于处理程序中错误和异常情况的一种机制。如上图所示异常主要分为两类,包括Error和Exception,两种异常有一个共同的父类是Throwable;在Exception中又分为RuntimeException(运行时异常)和CheckedExc 【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂! AllowM 算法hot100leetcodejava算法 [LeetCodeHot100]除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!✏️本文对应题目链接:除自身以外数组的乘积题目描述给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目数据保证数组之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。请不要使用除法,且在O(n)时间复杂度内完成 AGP 8.0 适配 ---- jvm target compatibility zhuzhumouse androidjavagradle 基础知识科普Android工程的GradleJDK,以及代码里面配置的jvmTarget有什么区别?这是两个完全不同的概念:GradleJDK:指的是用于运行Gradle构建系统的JavaDevelopmentKit版本。Gradle是Android项目的构建工具,它负责处理项目的编译、打包、依赖管理等任务。GradleJDK的选择决定了在构建过程中使用的Java版本。jvmTarget:是jav Java取绝对值 web15085181368 java 在Java中可以使用Math.abs()方法来方便的进行绝对值计算,例如:intvalue=Math.abs(-90);System.out.println(value);//90publicstaticintabs(inta){return(a<0)?-a:a;}当输入的是正数的时候直接返回即可,当是负数的时候返回它的相反数即可。使用三目运算符可以使用一行代码就能做到。如果需要输入Double或 RxJava 和Kotlin协程(Coroutines) Marblog JavaAndroidrxjavakotlinandroid RxJava和协程(Coroutines)都是处理异步编程和并发任务的强大工具,但它们的设计理念、使用方式和应用场景有所不同。以下是它们之间的主要区别:1.设计理念与核心概念RxJava:基于响应式流:RxJava是基于反应式编程(ReactiveProgramming)理念的库,它主要用于处理异步数据流和事件流。RxJava提供了丰富的操作符来对数据流进行组合、变换、过滤、错误处理等操作。数据流 强势破局:基于Java的开源能源管理系统源码+能碳管理系统+能源管理系统+能源管理平台:智碳能源管理系统,我为地球降一度!开启智慧节能新纪元,让能源管理“碳”索未来!可在线体验 智碳未来科技有限公司 能源开源javaspringboot物联网 先上干货!墙内仓库地址(码云):https://gitee.com/ustcyc/zhitan-ems已同步更新到github仓库:https://github.com/Andy-Yin/zhitan-ems在当今社会,能源管理已经成为一个不可忽视的重要议题。随着科技的不断发展,人们对能源的需求也日益增长,而能源的消耗对地球环境造成的影响也日益显现。为了更好地管理和利用能源资源,推动绿色可持续发展 Redis在实际应用中的最佳实践:缓存加速、分布式锁与消息队列 一碗黄焖鸡三碗米饭 Redis技术全景解析redisjava后端架构微服务 Redis在实际应用中的最佳实践:缓存加速、分布式锁与消息队列Redis作为一个高性能的内存数据库,凭借其高吞吐量、低延迟的特性,已成为开发者在构建现代应用时的首选技术之一。无论是在缓存加速、分布式锁,还是消息队列等多个应用场景中,Redis都展现出了卓越的性能。本文将围绕这三个核心场景,深入探讨Redis的最佳实践,帮助开发者更好地理解和应用Redis,在生产环境中提高系统的响应速度、稳定性与可 Redis基础笔记 JustGopher redis笔记java 一、基础知识连接方式CLI(CommandLineInterface)API(ApplicationProgrammingInterface)GUI(GraphicalUserInterface)启动redis-server连接到Redis(RedisCLIClient)redisredis-clitelnet127.0.0.16379退出quit/exit查看过期时间TTLkey设置过期时间ex Ubuntu 在/etc/profile中配置环境变量后,开启新终端窗口环境变量失效问题解决(非root用户) 深度视觉机器 Ubuntu20环境变量失效问题到此解决。 最近在使用ubuntu系统的过程中用非root用户登录,发现在安装完jdk并添加相关环境变量,java-version可以正常显示,但当我开启一个新的终端的时候,这个命令就失效了,网上查了一下原因,是需要修改当前登录用户的.bashrc文件中添加环境变量才可永久生效,下面来说明几种解决方案。1、临时解决方案(非root用户)临时解决方案就是重新加载/etc/profile文件,执行以下命令:$so 【系列专栏】银行IT的云原生架构-云单元架构 12 呱牛do it 金融科技云原生架构金融 银行IT的云原生架构-云单元架构一、引言在银行数字化转型进程中,云原生架构已成为提升竞争力、实现高效创新的关键支撑。其中,云单元架构作为一种先进的架构模式,正逐渐受到银行的关注与应用。云单元架构通过将复杂的系统拆分为多个相对独立、自治的单元,为银行带来了更高的灵活性、扩展性与可靠性。从目标、特征、单元化流量路由、应用与数据单元化、分布式中间件等多个关键角度深入剖析云单元架构,对于银行更好地理解和应 Mac Java 使用 tesseract 进行 ORC 识别 nukix macosjavamacosjava开发语言ORC 在Java开发中使用图片转文字时,难免会遇到问题,比如我使用Mac(M1芯片)系统进行开发,就出现报错。博主博客https://blog.uso6.comhttps://blog.csdn.net/dxk539687357一、直接使用1.使用brew进行安装brewinstalltesseract如果是其他系统的,建议看官方文档进行安装。2.查看版本nukix@nukixPC~%tesseract 音频采集(VUE3+JAVA) a26637896 音视频javajavascript vue部分代码xx.vueimportRecorderfrom'./Recorder.js';exportdefault{data(){return{mediaStream:null,recorder:null,isRecording:false,audioChunks:[],vadInterval:null//新增:用于存储声音活动检测的间隔ID};},asyncmounted(){this.m 渗透测试工具包 开源安全测试工具 网络安全工具_网络安全渗透测试工具(1) 2401_84254011 程序员安全开源测试工具 hackUtils-它是一个用于渗透测试和网络安全研究的黑客工具包,渗透以及web攻击脚本。msf框架:pocsscan攻击框架Pocsuite攻击框架Beebeeto攻击框架漏洞POC&EXP-ExploitDB官方git版本。php漏洞代码分析ysoserial-JAVA反序列化POC生成工具:JavaUnserializeExploits-JAVA反序列化EXP。JenkinsCommonC Java基础专项复习7——事务 Ttang23 java开发语言 Java基础专项复习系列目录1、Java基础专项复习1——函数关键字static-CSDN博客2、Java基础专项复习2——集合-CSDN博客3、Java基础专项复习3——Map集合-CSDN博客4、Java基础专项复习4——IO流-CSDN博客5、Java基础专项复习5——异常-CSDN博客6、Java基础专项复习6——多线程-CSDN博客文章目录目录Java基础专项复习系列目录文章目录1.1事 Android Java创建ViewModel新api debug_cat Android应用层开发androidjavaleetcode 背景项目使用Java,创建ViewModel发现之前旧api不管用了。不要问为什么项目还要用Java,别问。老项目不让升级。ViewModel创建新方式新方式是因为依赖新版本库,其实用旧版本库就回到旧方式了。依赖:deflifecycle_version="2.5.0"//ViewModelimplementation"androidx.lifecycle:lifecycle-viewmodel: 【Spring】Spring的模块架构与生态圈—Spring Boot、Spring Cloud与Spring Security AI人H哥会Java JAVAjavaspring后端开发语言springbootspringcloud 随着互联网的发展,企业对快速开发和高可用性的需求不断增加,Spring生态系统(包括SpringBoot、SpringCloud和SpringSecurity)应运而生,为Java开发提供了强大的支持。在实际应用中,SpringBoot使得开发者能够快速构建独立的、生产级的Spring应用;SpringCloud则为微服务架构提供了完整的解决方案;而SpringSecurity则为应用提供了安全保 Linux、Docker、Redis常见面试题 百百味 linuxdockerredis 1.Linux什么是Linux?Linux是一种基于UNIX的操作系统,最初是由LinusTorvalds引入的。它基于Linux内核,可以运行在由Intel,MIPS,HP,IBM,SPARC和Motorola制造的不同硬件平台上。Linux中另一个受欢迎的元素是它的吉祥物,一个名叫Tux的企鹅形象。UNIX和LINUX有什么区别?Unix最初是作为BellLaboratories的专有操作系统 Python学习心得体会 yuetouwen pythonwindows开发语言 一、引言Python作为一种高级编程语言,以其简洁性、易读性和强大的功能在当今的编程领域中占据着重要地位。在学习Python的过程中,我不仅掌握了一种新的编程工具,更深入地理解了编程的思维方式和逻辑结构。二、语法基础与编程环境搭建Python的语法简洁明了,相较于其他编程语言,其代码更接近自然语言。例如,使用缩进来表示代码块,而不是像C或Java那样使用大括号。在学习初期,我快速掌握了变量的定义、 【Java】—— 包装类&泛型 两袖清风998 数据结构与算法java开发语言 一、包装类(WrapperClass)1、包装类的定义在Java中数据分为两大类:基础数据类型(内置数据类型)引用类型其中基础数据类型(byteintlongdoublebooleanchar......)和C语言接近是早期为了吸引C语言的程序员。后续Java有引入了一系列更进阶的语法机制,而这些机制又依赖于引用类型。Java希望把所有的类型统一成Object体系,让基础数据类型也可以使用equa JSP(学习自用) 文城521 JAVA实训java学习html前端 一、本质JSP解析后就是Servlet类的java代码。二、jsp内嵌java代码1、声明脚本用于声明属性和方法。2、运行脚本相当于在service方法中写代码3、打印脚本用于打印上面两个脚本内声明的变量结果啥的。//页面会显示张三三、jsp内置对象1、request代表客户端的请求。2、response代表服务端的响应。3、session代表客户端当前会话。4、application代表整个We Effective Java学习笔记 lucky。 Java学习java 静态工厂方法考虑使用静态工厂方法代替构造静态工厂方法与构造器不同的第一优势在于,它们有名字第二个优势,不用每次被调用时都创建新对象第三个优势,可以返回原返回类型的子类第四个优势,在创建带泛型的实例时,能使代码变得简洁(jdk1.8已经解决)除此之外可以有多个参数相同但名称不同的工厂方法可以减少对外暴露的属性多了一层控制,方便统一修改Java中,获得一个类实例最简单的方法就是使用new关键字,通过构 java 代码走查_代码走查如何保证软件质量 weixin_40006965 java代码走查 目的代码走查的好处非常多,第一个是让新同学快速熟悉代码并了解系统。第二个是做资损防控的事前检查,在事前规避引发线上故障。第三个是通过一起讨论和审查,加强团队代码阅读和编写能力,让大家编写出优秀的代码。代码走查的优点非常多,但是最核心的还是希望通过代码走查提前发现问题并解决问题。所以基于以上目的,代码走查不是为了找到代码写的差的程序员加以批评,不是为了找到差的代码,而是一起发现问题共同成长,所以对于 mongoDB分片集群部署 glnullops 运维mongodb数据库nosql 一、MongoDB背景MongoDB是一款功能完善的分布式文档数据库,是一款非常出名的NoSQL数据库。当前国内使用Mongodb的大型实践越来越多,MongoDB为我司提供了重要的数据库存储服务,支撑着每天近千万级QPS峰值读写,数万亿级数据量存储服务。MongoDB在高性能、动态扩缩容、高可用、易部署、易使用、海量数据存储等方面拥有很大优势。近些年,MongoDB在DB-Engines流行度排 【Java基础-47.1】Java中通过继承Thread类创建线程 AllenBright #Java基础java开发语言 在Java中,多线程编程是实现并发操作的重要手段之一。Java提供了多种创建线程的方式,其中一种是通过继承Thread类来创建线程。本文将详细介绍如何通过继承Thread类创建线程,并探讨其使用场景、优缺点以及注意事项。1.什么是Thread类?Thread类是Java中用于表示线程的核心类。它位于java.lang包中,提供了线程的创建、启动、暂停、中断等操作。每个Thread对象都代表一个独立 java代码走查_java代码开发完成后,代码走查规范 游凯超 java代码走查 代码走查注意事项:1、不变的值,尽量写个常量类2、尽量使用if{}else,不要一直if去判断3、减少循环调用方法查询数据库4、dao层尽量不要用逻辑,尽量在service里写业务逻辑5、金额使用Bigdecimal类型的,0.00这种格式靠右显示6、iframe的弹框,要放到js里,可以缓存,放到jsp里每次都需要加载。7、ajax对应success对应一个error异常,尽量用error,网络 java 代码走查_java代码走查计划书 沙鸥123 java代码走查 《java代码走查计划书》由会员分享,可在线阅读,更多相关《java代码走查计划书(8页珍藏版)》请在人人文库网上搜索。1、WATERCorporation代码走查计划书Version2.0XXX2012/3/20文档修改记录版本号主要作者修改记录完成日期1.0无2010-03-181.1待评审物2010-03-181.2评审流程2010-03-192.0人员分工、评审流程2010-03-20目录 PHP,安卓,UI,java,linux视频教程合集 cocos2d-x小菜 javaUIlinuxPHPandroid ╔-----------------------------------╗┆ zookeeper admin 笔记 braveCS zookeeper Required Software 1) JDK>=1.6 2)推荐使用ensemble的ZooKeeper(至少3台),并run on separate machines 3)在Yahoo!,zk配置在特定的RHEL boxes里,2个cpu,2G内存,80G硬盘 数据和日志目录 1)数据目录里的文件是zk节点的持久化备份,包括快照和事务日 Spring配置多个连接池 easterfly spring 项目中需要同时连接多个数据库的时候,如何才能在需要用到哪个数据库就连接哪个数据库呢? Spring中有关于dataSource的配置: <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" &nb Mysql 171815164 mysql 例如,你想myuser使用mypassword从任何主机连接到mysql服务器的话。 GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%'IDENTIFIED BY 'mypassword' WI TH GRANT OPTION; 如果你想允许用户myuser从ip为192.168.1.6的主机连接到mysql服务器,并使用mypassword作 CommonDAO(公共/基础DAO) g21121 DAO 好久没有更新博客了,最近一段时间工作比较忙,所以请见谅,无论你是爱看呢还是爱看呢还是爱看呢,总之或许对你有些帮助。 DAO(Data Access Object)是一个数据访问(顾名思义就是与数据库打交道)接口,DAO一般在业 直言有讳 永夜-极光 感悟随笔 1.转载地址:http://blog.csdn.net/jasonblog/article/details/10813313 精华: “直言有讳”是阿里巴巴提倡的一种观念,而我在此之前并没有很深刻的认识。为什么呢?就好比是读书时候做阅读理解,我喜欢我自己的解读,并不喜欢老师给的意思。在这里也是。我自己坚持的原则是互相尊重,我觉得阿里巴巴很多价值观其实是基本的做人 安装CentOS 7 和Win 7后,Win7 引导丢失 随便小屋 centos 一般安装双系统的顺序是先装Win7,然后在安装CentOS,这样CentOS可以引导WIN 7启动。但安装CentOS7后,却找不到Win7 的引导,稍微修改一点东西即可。 一、首先具有root 的权限。 即进入Terminal后输入命令su,然后输入密码即可 二、利用vim编辑器打开/boot/grub2/grub.cfg文件进行修改 v Oracle备份与恢复案例 aijuans oracle Oracle备份与恢复案例 一. 理解什么是数据库恢复当我们使用一个数据库时,总希望数据库的内容是可靠的、正确的,但由于计算机系统的故障(硬件故障、软件故障、网络故障、进程故障和系统故障)影响数据库系统的操作,影响数据库中数据的正确性,甚至破坏数据库,使数据库中全部或部分数据丢失。因此当发生上述故障后,希望能重构这个完整的数据库,该处理称为数据库恢复。恢复过程大致可以分为复原(Restore)与 JavaEE开源快速开发平台G4Studio v5.0发布 無為子 我非常高兴地宣布,今天我们最新的JavaEE开源快速开发平台G4Studio_V5.0版本已经正式发布。 访问G4Studio网站 http://www.g4it.org 2013-04-06 发布G4Studio_V5.0版本 功能新增 (1). 新增了调用Oracle存储过程返回游标,并将游标映射为Java List集合对象的标 Oracle显示根据高考分数模拟录取 百合不是茶 PL/SQL编程oracle例子模拟高考录取学习交流 题目要求: 1,创建student表和result表 2,pl/sql对学生的成绩数据进行处理 3,处理的逻辑是根据每门专业课的最低分线和总分的最低分数线自动的将录取和落选 1,创建student表,和result表 学生信息表; create table student( student_id number primary key,--学生id 优秀的领导与差劲的领导 bijian1013 领导管理团队 责任 优秀的领导:优秀的领导总是对他所负责的项目担负起责任。如果项目不幸失败了,那么他知道该受责备的人是他自己,并且敢于承认错误。 差劲的领导:差劲的领导觉得这不是他的问题,因此他会想方设法证明是他的团队不行,或是将责任归咎于团队中他不喜欢的那几个成员身上。 努力工作 优秀的领导:团队领导应该是团队成员的榜样。至少,他应该与团队中的其他成员一样努力工作。这仅仅因为他 js函数在浏览器下的兼容 Bill_chen jquery浏览器IEDWRext 做前端开发的工程师,少不了要用FF进行测试,纯js函数在不同浏览器下,名称也可能不同。对于IE6和FF,取得下一结点的函数就不尽相同: IE6:node.nextSibling,对于FF是不能识别的; FF:node.nextElementSibling,对于IE是不能识别的; 兼容解决方式:var Div = node.nextSibl 【JVM四】老年代垃圾回收:吞吐量垃圾收集器(Throughput GC) bit1129 垃圾回收 吞吐量与用户线程暂停时间 衡量垃圾回收算法优劣的指标有两个: 吞吐量越高,则算法越好 暂停时间越短,则算法越好 首先说明吞吐量和暂停时间的含义。 垃圾回收时,JVM会启动几个特定的GC线程来完成垃圾回收的任务,这些GC线程与应用的用户线程产生竞争关系,共同竞争处理器资源以及CPU的执行时间。GC线程不会对用户带来的任何价值,因此,好的GC应该占 J2EE监听器和过滤器基础 白糖_ J2EE Servlet程序由Servlet,Filter和Listener组成,其中监听器用来监听Servlet容器上下文。 监听器通常分三类:基于Servlet上下文的ServletContex监听,基于会话的HttpSession监听和基于请求的ServletRequest监听。 ServletContex监听器 ServletContex又叫application 博弈AngularJS讲义(16) - 提供者 boyitech jsAngularJSapiAngularProvider Angular框架提供了强大的依赖注入机制,这一切都是有注入器(injector)完成. 注入器会自动实例化服务组件和符合Angular API规则的特殊对象,例如控制器,指令,过滤器动画等。 那注入器怎么知道如何去创建这些特殊的对象呢? Angular提供了5种方式让注入器创建对象,其中最基础的方式就是提供者(provider), 其余四种方式(Value, Fac java-写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 bylijinnan java public class CommonSubSequence { /** * 题目:写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 * 写一个版本算法复杂度O(N^2)和一个O(N) 。 * * O(N^2):对于a中的每个字符,遍历b中的每个字符,如果相同,则拷贝到新字符串中。 * O( sqlserver 2000 无法验证产品密钥 Chen.H sqlwindowsSQL ServerMicrosoft 在 Service Pack 4 (SP 4), 是运行 Microsoft Windows Server 2003、 Microsoft Windows Storage Server 2003 或 Microsoft Windows 2000 服务器上您尝试安装 Microsoft SQL Server 2000 通过卷许可协议 (VLA) 媒体。 这样做, 收到以下错误信息CD KEY的 SQ [新概念武器]气象战争 comsci 气象战争的发动者必须是拥有发射深空航天器能力的国家或者组织.... 原因如下: 地球上的气候变化和大气层中的云层涡旋场有密切的关系,而维持一个在大气层某个层次 oracle 中 rollup、cube、grouping 使用详解 daizj oraclegroupingrollupcube oracle 中 rollup、cube、grouping 使用详解 -- 使用oracle 样例表演示 转自namesliu -- 使用oracle 的样列库,演示 rollup, cube, grouping 的用法与使用场景 --- ROLLUP , 为了理解分组的成员数量,我增加了 分组的计数 COUNT(SAL) 技术资料汇总分享 Dead_knight 技术资料汇总 分享 本人汇总的技术资料,分享出来,希望对大家有用。 http://pan.baidu.com/s/1jGr56uE 资料主要包含: Workflow->工作流相关理论、框架(OSWorkflow、JBPM、Activiti、fireflow...) Security->java安全相关资料(SSL、SSO、SpringSecurity、Shiro、JAAS...) Ser 初一下学期难记忆单词背诵第一课 dcj3sjt126com englishword could 能够 minute 分钟 Tuesday 星期二 February 二月 eighteenth 第十八 listen 听 careful 小心的,仔细的 short 短的 heavy 重的 empty 空的 certainly 当然 carry 携带;搬运 tape 磁带 basket 蓝子 bottle 瓶 juice 汁,果汁 head 头;头部 截取视图的图片, 然后分享出去 dcj3sjt126com OSObjective-C OS 7 has a new method that allows you to draw a view hierarchy into the current graphics context. This can be used to get an UIImage very fast. I implemented a category method on UIView to get the vi MySql重置密码 fanxiaolong MySql重置密码 方法一: 在my.ini的[mysqld]字段加入: skip-grant-tables 重启mysql服务,这时的mysql不需要密码即可登录数据库 然后进入mysql mysql>use mysql; mysql>更新 user set password=password('新密码') WHERE User='root'; mysq Ehcache(03)——Ehcache中储存缓存的方式 234390216 ehcacheMemoryStoreDiskStore存储驱除策略 Ehcache中储存缓存的方式 目录 1 堆内存(MemoryStore) 1.1 指定可用内存 1.2 驱除策略 1.3 元素过期 2 &nbs spring mvc中的@propertysource jackyrong spring mvc 在spring mvc中,在配置文件中的东西,可以在java代码中通过注解进行读取了: @PropertySource 在spring 3.1中开始引入 比如有配置文件 config.properties mongodb.url=1.2.3.4 mongodb.db=hello 则代码中 @PropertySource(& 重学单例模式 lanqiu17 单例Singleton模式 最近在重新学习设计模式,感觉对模式理解更加深刻。觉得有必要记下来。 第一个学的就是单例模式,单例模式估计是最好理解的模式了。它的作用就是防止外部创建实例,保证只有一个实例。 单例模式的常用实现方式有两种,就人们熟知的饱汉式与饥汉式,具体就不多说了。这里说下其他的实现方式 静态内部类方式: package test.pattern.singleton.statics; publ .NET开源核心运行时,且行且珍惜 netcome java.net开源 背景 2014年11月12日,ASP.NET之父、微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣布,微软将开源全部.NET核心运行时,并将.NET 扩展为可在 Linux 和 Mac OS 平台上运行。.NET核心运行时将基于MIT开源许可协议发布,其中将包括执行.NET代码所需的一切项目——CLR、JIT编译器、垃圾收集器(GC)和核心 使用oscahe缓存技术减少与数据库的频繁交互 Everyday都不同 Web高并发oscahe缓存 此前一直不知道缓存的具体实现,只知道是把数据存储在内存中,以便下次直接从内存中读取。对于缓存的使用也没有概念,觉得缓存技术是一个比较”神秘陌生“的领域。但最近要用到缓存技术,发现还是很有必要一探究竟的。 缓存技术使用背景:一般来说,对于web项目,如果我们要什么数据直接jdbc查库好了,但是在遇到高并发的情形下,不可能每一次都是去查数据库,因为这样在高并发的情形下显得不太合理—— Spring+Mybatis 手动控制事务 toknowme mybatis @Override public boolean testDelete(String jobCode) throws Exception { boolean flag = false; &nbs 菜鸟级的android程序员面试时候需要掌握的知识点 xp9802 android 熟悉Android开发架构和API调用 掌握APP适应不同型号手机屏幕开发技巧 熟悉Android下的数据存储 熟练Android Debug Bridge Tool 熟练Eclipse/ADT及相关工具 熟悉Android框架原理及Activity生命周期 熟练进行Android UI布局 熟练使用SQLite数据库; 熟悉Android下网络通信机制,S 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。
严谨的小张觉得虽然当个普通互斥锁,已经稳稳够用,可是业务里总是又很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题。
而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。
小张不是杠精,因为库存操作总有这样那样的特殊。
所以我们希望在这种情况时,可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果,这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约。
读写分离也是常见,一个读多写少的业务为了性能,常常是有读锁和写锁的。
而此刻的扩展已经超出了一把简单轮子的复杂程度,光是处理续约,就够小张喝一壶,何况在性能(锁的最大等待时间)、优雅(无效锁申请)、重试(失败重试机制)等方面还要下功夫研究。
在小张苦思冥想时,旁边的小白凑过来看了看小张,很好奇,都2021年了,为什么不直接用redisson呢?
Redisson就有这把你要的锁。
号称简单的Redisson分布式锁的使用姿势是什么?
org.redisson redisson 3.13.6 org.redisson redisson-spring-boot-starter 3.13.6
@Configuration public class RedissionConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.password}") private String password; private int port = 6379; @Bean public RedissonClient getRedisson() { Config config = new Config(); config.useSingleServer(). setAddress("redis://" + redisHost + ":" + port). setPassword(password); config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); } }
@Resource private RedissonClient redissonClient; RLock rLock = redissonClient.getLock(lockName); try { boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS); if (isLocked) { // TODO } } catch (Exception e) { rLock.unlock(); }
简洁明了,只需要一个RLock,既然推荐Redisson,就往里面看看他是怎么实现的。
RLock是Redisson分布式锁的最核心接口,继承了concurrent包的Lock接口和自己的RLockAsync接口,RLockAsync的返回值都是RFuture,是Redisson执行异步实现的核心逻辑,也是Netty发挥的主要阵地。
RLock如何加锁?
从RLock进入,找到RedissonLock类,找到tryLock方法再递进到干事的tryAcquireOnceAsync方法,这是加锁的主要代码(版本不一此处实现有差别,和最新3.15.x有一定出入,但是核心逻辑依然未变。此处以3.13.6为例)
private RFuture tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else { RFuture ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } }
此处出现leaseTime时间判断的2个分支,实际上就是加锁时是否设置过期时间,未设置过期时间(-1)时则会有watchDog的锁续约(下文),一个注册了加锁事件的续约任务。我们先来看有过期时间tryLockInnerAsync部分,
evalWriteAsync是eval命令执行lua的入口
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); }
这里揭开真面目,eval命令执行Lua脚本的地方,此处的Lua脚本展开
-- 不存在该key时 if (redis.call('exists', KEYS[1]) == 0) then -- 新增该锁并且hash中该线程id对应的count置1 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间 redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 存在该key 并且 hash中线程id的key也存在 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 线程重入次数++ redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);
和前面我们写自定义的分布式锁的脚本几乎一致,看来redisson也是一样的实现,具体参数分析:
// keyName KEYS[1] = Collections.singletonList(this.getName()) // leaseTime ARGV[1] = this.internalLockLeaseTime // uuid+threadId组合的唯一值 ARGV[2] = this.getLockName(threadId)
总共3个参数完成了一段逻辑:
判断该锁是否已经有对应hash表存在,
• 没有对应的hash表:则set该hash表中一个entry的key为锁名称,value为1,之后设置该hash表失效时间为leaseTime
• 存在对应的hash表:则将该lockName的value执行+1操作,也就是计算进入次数,再设置失效时间leaseTime
• 最后返回这把锁的ttl剩余时间
也和上述自定义锁没有区别
既然如此,那解锁的步骤也肯定有对应的-1操作,再看unlock方法,同样查找方法名,一路到
protected RFuture unlockInnerAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)}); }
掏出Lua部分
-- 不存在key if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; -- 计数器 -1 local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then -- 过期时间重设 redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else -- 删除并发布解锁消息 redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
该Lua KEYS有2个Arrays.asList(getName(), getChannelName())
name 锁名称 channelName,用于pubSub发布消息的channel名称
ARGV变量有三个LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)
LockPubSub.UNLOCK_MESSAGE,channel发送消息的类别,此处解锁为0
internalLockLeaseTime,watchDog配置的超时时间,默认为30s
lockName 这里的lockName指的是uuid和threadId组合的唯一值
步骤如下:
1.如果该锁不存在则返回nil;
2.如果该锁存在则将其线程的hash key计数器-1,
3.计数器counter>0,重置下失效时间,返回0;否则,删除该锁,发布解锁消息unlockMessage,返回1;
其中unLock的时候使用到了Redis发布订阅PubSub完成消息通知。
而订阅的步骤就在RedissonLock的加锁入口的lock方法里
long threadId = Thread.currentThread().getId(); Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId); if (ttl != null) { // 订阅 RFuture future = this.subscribe(threadId); if (interruptibly) { this.commandExecutor.syncSubscriptionInterrupted(future); } else { this.commandExecutor.syncSubscription(future); } // 省略
当锁被其他线程占用时,通过监听锁的释放通知(在其他线程通过RedissonLock释放锁时,会通过发布订阅pub/sub功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。
为了一探究竟通知了什么,通知后又做了什么,进入LockPubSub。
这里只有一个明显的监听方法onMessage,其订阅和信号量的释放都在父类PublishSubscribe,我们只关注监听事件的实际操作
protected void onMessage(RedissonLockEntry value, Long message) { Runnable runnableToExecute; if (message.equals(unlockMessage)) { // 从监听器队列取监听线程执行监听回调 runnableToExecute = (Runnable)value.getListeners().poll(); if (runnableToExecute != null) { runnableToExecute.run(); } // getLatch()返回的是Semaphore,信号量,此处是释放信号量 // 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁 value.getLatch().release(); } else if (message.equals(readUnlockMessage)) { while(true) { runnableToExecute = (Runnable)value.getListeners().poll(); if (runnableToExecute == null) { value.getLatch().release(value.getLatch().getQueueLength()); break; } runnableToExecute.run(); } } }
发现一个是默认解锁消息,一个是读锁解锁消息,因为redisson是有提供读写锁的,而读写锁读读情况和读写、写写情况互斥情况不同,我们只看上面的默认解锁消息unlockMessage分支
LockPubSub监听最终执行了2件事
runnableToExecute.run() 执行监听回调
value.getLatch().release(); 释放信号量
Redisson通过LockPubSub监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁。
这时再回来看tryAcquireOnceAsync另一分支
可以看到,无超时时间时,在执行加锁操作后,还执行了一段费解的逻辑
ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }) } } })
此处涉及到Netty的Future/Promise-Listener模型,Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解
在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。
这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务。
这段定时任务的就是watchDog的核心。
查看RedissonLock.this.scheduleExpirationRenewal(threadId)
private void scheduleExpirationRenewal(long threadId) { RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry(); RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry); if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); this.renewExpiration(); } } private void renewExpiration() { RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName()); if (ee != null) { Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() { public void run(Timeout timeout) throws Exception { RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName()); if (ent != null) { Long threadId = ent.getFirstThreadId(); if (threadId != null) { RFuture future = RedissonLock.this.renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e); } else { if (res) { RedissonLock.this.renewExpiration(); } } }); } } } }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); ee.setTimeout(task); } }
拆分来看,这段连续嵌套且冗长的代码实际上做了几步
• 添加一个netty的Timeout回调任务,每(internalLockLeaseTime / 3)毫秒执行一次,执行的方法是renewExpirationAsync
• renewExpirationAsync重置了锁超时时间,又注册一个监听器,监听回调又执行了renewExpiration
renewExpirationAsync 的Lua如下
protected RFuture renewExpirationAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); } if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;
重新设置了超时时间。
Redisson加这段逻辑的目的是什么?
目的是为了某种场景下保证业务不影响,如任务执行超时但未结束,锁已经释放的问题。
当一个线程持有了一把锁,由于并未设置超时时间leaseTime,Redisson默认配置了30S,开启watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁。
这就是Redisson的锁续约,也就是WatchDog实现的基本思路。
通过整体的介绍,流程简单概括:
A、B线程争抢一把锁,A获取到后,B阻塞
B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息
A操作完成释放了锁,B线程收到订阅消息通知
B被唤醒开始继续抢锁,拿到锁
详细加锁解锁流程总结如下图:
以上介绍的可重入锁是非公平锁,Redisson还基于Redis的队列(List)和ZSet实现了公平
公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少
回顾JUC的ReentrantLock公平锁的实现
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
AQS已经提供了整个实现,是否公平取决于实现类取出节点逻辑是否顺序取
AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,通过内置FIFO队列来完成资源获取线程的排队工作,他自身没有实现同步接口,仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用(上图),支持独占和共享获取,这是基于模版方法模式的一种设计,给公平/非公平提供了土壤。
我们用2张图来简单解释AQS的等待流程(出自《JAVA并发编程的艺术》)
一张是同步队列(FIFO双向队列)管理 获取同步状态失败(抢锁失败)的线程引用、等待状态和前驱后继节点的流程图
一张是独占式获取同步状态的总流程,核心acquire(int arg)方法调用流程
可以看出锁的获取流程
AQS维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋,移出队列或停止自旋的条件是前驱节点为头节点切成功获取了同步状态。
而比较另一段非公平锁类NonfairSync可以发现,控制公平和非公平的关键代码,在于hasQueuedPredecessors方法。
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
NonfairSync减少了了hasQueuedPredecessors判断条件,该方法的作用就是
查看同步队列中当前节点是否有前驱节点,如果有比当前线程更早请求获取锁则返回true。
保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则
因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。
可见公平的代价是性能与吞吐量。
Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。
RedissonFairLock 用法依然很简单
RLock fairLock = redissonClient.getFairLock(lockName); fairLock.lock();
RedissonFairLock继承自RedissonLock,同样一路向下找到加锁实现方法tryLockInnerAsync。
这里有2段冗长的Lua,但是Debug发现,公平锁的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua较长,参数也多,我们着重分析Lua的实现规则
参数:
-- lua中的几个参数 KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName) KEYS[1]: lock_name, 锁名称 KEYS[2]: "redisson_lock_queue:{xxx}" 线程队列 KEYS[3]: "redisson_lock_timeout:{xxx}" 线程id对应的超时集合 ARGV = internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime ARGV[1]: "{leaseTime}" 过期时间 ARGV[2]: "{Redisson.UUID}:{threadId}" ARGV[3] = 当前时间 + 线程等待时间:(10:00:00) + 5000毫秒 = 10:00:05 ARGV[4] = 当前时间(10:00:00) 部署服务器时间,非redis-server服务器时间 公平锁实现的Lua脚本 -- 1.死循环清除过期key while true do -- 获取头节点 local firstThreadId2 = redis.call('lindex', KEYS[2], 0); -- 首次获取必空跳出循环 if firstThreadId2 == false then break; end; -- 清除过期key local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2)); if timeout <= tonumber(ARGV[4]) then redis.call('zrem', KEYS[3], firstThreadId2); redis.call('lpop', KEYS[2]); else break; end; end; -- 2.不存在该锁 && (不存在线程等待队列 || 存在线程等待队列而且第一个节点就是此线程ID),加锁部分主要逻辑 if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then -- 弹出队列中线程id元素,删除Zset中该线程id对应的元素 redis.call('lpop', KEYS[2]); redis.call('zrem', KEYS[3], ARGV[2]); local keys = redis.call('zrange', KEYS[3], 0, -1); -- 遍历zSet所有key,将key的超时时间(score) - 当前时间ms for i = 1, #keys, 1 do redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]); end; -- 加锁设置锁过期时间 redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 3.线程存在,重入判断 if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then redis.call('hincrby', KEYS[1], ARGV[2],1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 4.返回当前线程剩余存活时间 local timeout = redis.call('zscore', KEYS[3], ARGV[2]); if timeout ~= false then -- 过期时间timeout的值在下方设置,此处的减法算出的依旧是当前线程的ttl return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]); end; -- 5.尾节点剩余存活时间 local lastThreadId = redis.call('lindex', KEYS[2], -1); local ttl; -- 尾节点不空 && 尾节点非当前线程 if lastThreadId ~= false and lastThreadId ~= ARGV[2] then -- 计算队尾节点剩余存活时间 ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]); else -- 获取lock_name剩余存活时间 ttl = redis.call('pttl', KEYS[1]); end; -- 6.末尾排队 -- zSet 超时时间(score),尾节点ttl + 当前时间 + 5000ms + 当前时间,无则新增,有则更新 -- 线程id放入队列尾部排队,无则插入,有则不再插入 local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then redis.call('rpush', KEYS[2], ARGV[2]); end; return ttl; 5.4 公平锁加锁步骤 通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。 其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤 1.队列清理 保证队列中只有未过期的等待线程 2.首次加锁 hset加锁,pexpire过期时间 3.重入判断 此处同可重入锁lua 4.返回ttl 5.计算尾节点ttl 初始值为锁的剩余过期时间 6.末尾排队 ttl + 2 * currentTime + waitTime是score的默认值计算公式 2.模拟 如果模拟以下顺序,就会明了redisson公平锁整个加锁流程 假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20 t1:当线程1初次获取锁 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束 t2:线程2尝试获取锁(线程1未释放锁) 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10 t3:线程3尝试获取锁(线程1未释放锁) 1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20 如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。 而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取) 10:00:30 线程2尝试获取锁(线程1已释放锁) 1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束 排队结构如图: 公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。 由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。 并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 6 总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。 Redisson也提供了多机情况下的联锁MultiLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock 和官方推荐的红锁RedLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(Java,redis,分布式,redis,redisson,java) Spring Boot:Java开发的神奇加速器(二) 小周不想卷 艾思科蓝学术会议投稿springboot 目录四、深入理解SpringBoot配置4.1配置文件类型4.2常用配置项4.3自定义配置五、数据访问与持久化5.1集成SpringDataJPA5.2编写数据访问层代码5.3事务管理四、深入理解SpringBoot配置4.1配置文件类型在SpringBoot应用中,主要有两种配置文件类型,即application.properties和application.yml(或application.y Jenkins 拉取 Git 分支代码问题排查与解决指南 码农阿豪@新空间 包罗万象疑难杂症解决方案git运维jenkins 个人名片作者简介:java领域优质创作者个人主页:码农阿豪工作室:新空间代码工作室(提供各种软件服务)个人邮箱:[2435024119@qq.com]个人微信:15279484656个人导航网站:www.forff.top座右铭:总有人要赢。为什么不能是我呢?专栏导航:码农阿豪系列专栏导航面试专栏:收集了java相关高频面试题,面试实战总结️Spring5系列专栏:整理了Spring5重要知识点与 如何发起http的请求,在系统中集成 红豆和绿豆 javahttp网络协议网络 在Java中发起HTTP请求,有多种开源框架可供选择。以下是一些常用的开源框架及其特点:1.ApacheHttpClientApacheHttpClient是一个功能强大的HTTP客户端库,支持同步和异步请求,广泛用于各种Java应用。2.OkHttpOkHttp是一个高效的HTTP客户端,支持HTTP/2和WebSocket,具有自动重试和恢复功能。3.UniRestUniRest是一个简单易用 Houdini:Houdini光照与渲染基础_2024-07-16_02-34-24.Tex chenjj4003 游戏开发houdiniandroidcinema4dblender游戏3dsmax Houdini:Houdini光照与渲染基础Houdini渲染引擎简介Mantra渲染器概述Mantra是Houdini自带的渲染引擎,它是一个基于物理的渲染器,能够处理复杂的光线追踪和全局光照效果。Mantra的设计理念是灵活性和可扩展性,它支持多种渲染模式,包括CPU渲染和GPU渲染,以及分布式渲染。Mantra的渲染质量高,特别适合于处理大规模的场景和复杂的视觉效果。Mantra渲染器的特点 创建全局异常处理器(Global Exception Processor) 易安杰 java开发语言jvmspringboot 先来了解一下什么是异常?在Java程序中,异常(Exception)是指在程序执行过程中发生的非正常情况,它打断了正常的指令流;Java中的异常处理是一种用于处理程序中错误和异常情况的一种机制。如上图所示异常主要分为两类,包括Error和Exception,两种异常有一个共同的父类是Throwable;在Exception中又分为RuntimeException(运行时异常)和CheckedExc 【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂! AllowM 算法hot100leetcodejava算法 [LeetCodeHot100]除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!✏️本文对应题目链接:除自身以外数组的乘积题目描述给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目数据保证数组之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。请不要使用除法,且在O(n)时间复杂度内完成 AGP 8.0 适配 ---- jvm target compatibility zhuzhumouse androidjavagradle 基础知识科普Android工程的GradleJDK,以及代码里面配置的jvmTarget有什么区别?这是两个完全不同的概念:GradleJDK:指的是用于运行Gradle构建系统的JavaDevelopmentKit版本。Gradle是Android项目的构建工具,它负责处理项目的编译、打包、依赖管理等任务。GradleJDK的选择决定了在构建过程中使用的Java版本。jvmTarget:是jav Java取绝对值 web15085181368 java 在Java中可以使用Math.abs()方法来方便的进行绝对值计算,例如:intvalue=Math.abs(-90);System.out.println(value);//90publicstaticintabs(inta){return(a<0)?-a:a;}当输入的是正数的时候直接返回即可,当是负数的时候返回它的相反数即可。使用三目运算符可以使用一行代码就能做到。如果需要输入Double或 RxJava 和Kotlin协程(Coroutines) Marblog JavaAndroidrxjavakotlinandroid RxJava和协程(Coroutines)都是处理异步编程和并发任务的强大工具,但它们的设计理念、使用方式和应用场景有所不同。以下是它们之间的主要区别:1.设计理念与核心概念RxJava:基于响应式流:RxJava是基于反应式编程(ReactiveProgramming)理念的库,它主要用于处理异步数据流和事件流。RxJava提供了丰富的操作符来对数据流进行组合、变换、过滤、错误处理等操作。数据流 强势破局:基于Java的开源能源管理系统源码+能碳管理系统+能源管理系统+能源管理平台:智碳能源管理系统,我为地球降一度!开启智慧节能新纪元,让能源管理“碳”索未来!可在线体验 智碳未来科技有限公司 能源开源javaspringboot物联网 先上干货!墙内仓库地址(码云):https://gitee.com/ustcyc/zhitan-ems已同步更新到github仓库:https://github.com/Andy-Yin/zhitan-ems在当今社会,能源管理已经成为一个不可忽视的重要议题。随着科技的不断发展,人们对能源的需求也日益增长,而能源的消耗对地球环境造成的影响也日益显现。为了更好地管理和利用能源资源,推动绿色可持续发展 Redis在实际应用中的最佳实践:缓存加速、分布式锁与消息队列 一碗黄焖鸡三碗米饭 Redis技术全景解析redisjava后端架构微服务 Redis在实际应用中的最佳实践:缓存加速、分布式锁与消息队列Redis作为一个高性能的内存数据库,凭借其高吞吐量、低延迟的特性,已成为开发者在构建现代应用时的首选技术之一。无论是在缓存加速、分布式锁,还是消息队列等多个应用场景中,Redis都展现出了卓越的性能。本文将围绕这三个核心场景,深入探讨Redis的最佳实践,帮助开发者更好地理解和应用Redis,在生产环境中提高系统的响应速度、稳定性与可 Redis基础笔记 JustGopher redis笔记java 一、基础知识连接方式CLI(CommandLineInterface)API(ApplicationProgrammingInterface)GUI(GraphicalUserInterface)启动redis-server连接到Redis(RedisCLIClient)redisredis-clitelnet127.0.0.16379退出quit/exit查看过期时间TTLkey设置过期时间ex Ubuntu 在/etc/profile中配置环境变量后,开启新终端窗口环境变量失效问题解决(非root用户) 深度视觉机器 Ubuntu20环境变量失效问题到此解决。 最近在使用ubuntu系统的过程中用非root用户登录,发现在安装完jdk并添加相关环境变量,java-version可以正常显示,但当我开启一个新的终端的时候,这个命令就失效了,网上查了一下原因,是需要修改当前登录用户的.bashrc文件中添加环境变量才可永久生效,下面来说明几种解决方案。1、临时解决方案(非root用户)临时解决方案就是重新加载/etc/profile文件,执行以下命令:$so 【系列专栏】银行IT的云原生架构-云单元架构 12 呱牛do it 金融科技云原生架构金融 银行IT的云原生架构-云单元架构一、引言在银行数字化转型进程中,云原生架构已成为提升竞争力、实现高效创新的关键支撑。其中,云单元架构作为一种先进的架构模式,正逐渐受到银行的关注与应用。云单元架构通过将复杂的系统拆分为多个相对独立、自治的单元,为银行带来了更高的灵活性、扩展性与可靠性。从目标、特征、单元化流量路由、应用与数据单元化、分布式中间件等多个关键角度深入剖析云单元架构,对于银行更好地理解和应 Mac Java 使用 tesseract 进行 ORC 识别 nukix macosjavamacosjava开发语言ORC 在Java开发中使用图片转文字时,难免会遇到问题,比如我使用Mac(M1芯片)系统进行开发,就出现报错。博主博客https://blog.uso6.comhttps://blog.csdn.net/dxk539687357一、直接使用1.使用brew进行安装brewinstalltesseract如果是其他系统的,建议看官方文档进行安装。2.查看版本nukix@nukixPC~%tesseract 音频采集(VUE3+JAVA) a26637896 音视频javajavascript vue部分代码xx.vueimportRecorderfrom'./Recorder.js';exportdefault{data(){return{mediaStream:null,recorder:null,isRecording:false,audioChunks:[],vadInterval:null//新增:用于存储声音活动检测的间隔ID};},asyncmounted(){this.m 渗透测试工具包 开源安全测试工具 网络安全工具_网络安全渗透测试工具(1) 2401_84254011 程序员安全开源测试工具 hackUtils-它是一个用于渗透测试和网络安全研究的黑客工具包,渗透以及web攻击脚本。msf框架:pocsscan攻击框架Pocsuite攻击框架Beebeeto攻击框架漏洞POC&EXP-ExploitDB官方git版本。php漏洞代码分析ysoserial-JAVA反序列化POC生成工具:JavaUnserializeExploits-JAVA反序列化EXP。JenkinsCommonC Java基础专项复习7——事务 Ttang23 java开发语言 Java基础专项复习系列目录1、Java基础专项复习1——函数关键字static-CSDN博客2、Java基础专项复习2——集合-CSDN博客3、Java基础专项复习3——Map集合-CSDN博客4、Java基础专项复习4——IO流-CSDN博客5、Java基础专项复习5——异常-CSDN博客6、Java基础专项复习6——多线程-CSDN博客文章目录目录Java基础专项复习系列目录文章目录1.1事 Android Java创建ViewModel新api debug_cat Android应用层开发androidjavaleetcode 背景项目使用Java,创建ViewModel发现之前旧api不管用了。不要问为什么项目还要用Java,别问。老项目不让升级。ViewModel创建新方式新方式是因为依赖新版本库,其实用旧版本库就回到旧方式了。依赖:deflifecycle_version="2.5.0"//ViewModelimplementation"androidx.lifecycle:lifecycle-viewmodel: 【Spring】Spring的模块架构与生态圈—Spring Boot、Spring Cloud与Spring Security AI人H哥会Java JAVAjavaspring后端开发语言springbootspringcloud 随着互联网的发展,企业对快速开发和高可用性的需求不断增加,Spring生态系统(包括SpringBoot、SpringCloud和SpringSecurity)应运而生,为Java开发提供了强大的支持。在实际应用中,SpringBoot使得开发者能够快速构建独立的、生产级的Spring应用;SpringCloud则为微服务架构提供了完整的解决方案;而SpringSecurity则为应用提供了安全保 Linux、Docker、Redis常见面试题 百百味 linuxdockerredis 1.Linux什么是Linux?Linux是一种基于UNIX的操作系统,最初是由LinusTorvalds引入的。它基于Linux内核,可以运行在由Intel,MIPS,HP,IBM,SPARC和Motorola制造的不同硬件平台上。Linux中另一个受欢迎的元素是它的吉祥物,一个名叫Tux的企鹅形象。UNIX和LINUX有什么区别?Unix最初是作为BellLaboratories的专有操作系统 Python学习心得体会 yuetouwen pythonwindows开发语言 一、引言Python作为一种高级编程语言,以其简洁性、易读性和强大的功能在当今的编程领域中占据着重要地位。在学习Python的过程中,我不仅掌握了一种新的编程工具,更深入地理解了编程的思维方式和逻辑结构。二、语法基础与编程环境搭建Python的语法简洁明了,相较于其他编程语言,其代码更接近自然语言。例如,使用缩进来表示代码块,而不是像C或Java那样使用大括号。在学习初期,我快速掌握了变量的定义、 【Java】—— 包装类&泛型 两袖清风998 数据结构与算法java开发语言 一、包装类(WrapperClass)1、包装类的定义在Java中数据分为两大类:基础数据类型(内置数据类型)引用类型其中基础数据类型(byteintlongdoublebooleanchar......)和C语言接近是早期为了吸引C语言的程序员。后续Java有引入了一系列更进阶的语法机制,而这些机制又依赖于引用类型。Java希望把所有的类型统一成Object体系,让基础数据类型也可以使用equa JSP(学习自用) 文城521 JAVA实训java学习html前端 一、本质JSP解析后就是Servlet类的java代码。二、jsp内嵌java代码1、声明脚本用于声明属性和方法。2、运行脚本相当于在service方法中写代码3、打印脚本用于打印上面两个脚本内声明的变量结果啥的。//页面会显示张三三、jsp内置对象1、request代表客户端的请求。2、response代表服务端的响应。3、session代表客户端当前会话。4、application代表整个We Effective Java学习笔记 lucky。 Java学习java 静态工厂方法考虑使用静态工厂方法代替构造静态工厂方法与构造器不同的第一优势在于,它们有名字第二个优势,不用每次被调用时都创建新对象第三个优势,可以返回原返回类型的子类第四个优势,在创建带泛型的实例时,能使代码变得简洁(jdk1.8已经解决)除此之外可以有多个参数相同但名称不同的工厂方法可以减少对外暴露的属性多了一层控制,方便统一修改Java中,获得一个类实例最简单的方法就是使用new关键字,通过构 java 代码走查_代码走查如何保证软件质量 weixin_40006965 java代码走查 目的代码走查的好处非常多,第一个是让新同学快速熟悉代码并了解系统。第二个是做资损防控的事前检查,在事前规避引发线上故障。第三个是通过一起讨论和审查,加强团队代码阅读和编写能力,让大家编写出优秀的代码。代码走查的优点非常多,但是最核心的还是希望通过代码走查提前发现问题并解决问题。所以基于以上目的,代码走查不是为了找到代码写的差的程序员加以批评,不是为了找到差的代码,而是一起发现问题共同成长,所以对于 mongoDB分片集群部署 glnullops 运维mongodb数据库nosql 一、MongoDB背景MongoDB是一款功能完善的分布式文档数据库,是一款非常出名的NoSQL数据库。当前国内使用Mongodb的大型实践越来越多,MongoDB为我司提供了重要的数据库存储服务,支撑着每天近千万级QPS峰值读写,数万亿级数据量存储服务。MongoDB在高性能、动态扩缩容、高可用、易部署、易使用、海量数据存储等方面拥有很大优势。近些年,MongoDB在DB-Engines流行度排 【Java基础-47.1】Java中通过继承Thread类创建线程 AllenBright #Java基础java开发语言 在Java中,多线程编程是实现并发操作的重要手段之一。Java提供了多种创建线程的方式,其中一种是通过继承Thread类来创建线程。本文将详细介绍如何通过继承Thread类创建线程,并探讨其使用场景、优缺点以及注意事项。1.什么是Thread类?Thread类是Java中用于表示线程的核心类。它位于java.lang包中,提供了线程的创建、启动、暂停、中断等操作。每个Thread对象都代表一个独立 java代码走查_java代码开发完成后,代码走查规范 游凯超 java代码走查 代码走查注意事项:1、不变的值,尽量写个常量类2、尽量使用if{}else,不要一直if去判断3、减少循环调用方法查询数据库4、dao层尽量不要用逻辑,尽量在service里写业务逻辑5、金额使用Bigdecimal类型的,0.00这种格式靠右显示6、iframe的弹框,要放到js里,可以缓存,放到jsp里每次都需要加载。7、ajax对应success对应一个error异常,尽量用error,网络 java 代码走查_java代码走查计划书 沙鸥123 java代码走查 《java代码走查计划书》由会员分享,可在线阅读,更多相关《java代码走查计划书(8页珍藏版)》请在人人文库网上搜索。1、WATERCorporation代码走查计划书Version2.0XXX2012/3/20文档修改记录版本号主要作者修改记录完成日期1.0无2010-03-181.1待评审物2010-03-181.2评审流程2010-03-192.0人员分工、评审流程2010-03-20目录 PHP,安卓,UI,java,linux视频教程合集 cocos2d-x小菜 javaUIlinuxPHPandroid ╔-----------------------------------╗┆ zookeeper admin 笔记 braveCS zookeeper Required Software 1) JDK>=1.6 2)推荐使用ensemble的ZooKeeper(至少3台),并run on separate machines 3)在Yahoo!,zk配置在特定的RHEL boxes里,2个cpu,2G内存,80G硬盘 数据和日志目录 1)数据目录里的文件是zk节点的持久化备份,包括快照和事务日 Spring配置多个连接池 easterfly spring 项目中需要同时连接多个数据库的时候,如何才能在需要用到哪个数据库就连接哪个数据库呢? Spring中有关于dataSource的配置: <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" &nb Mysql 171815164 mysql 例如,你想myuser使用mypassword从任何主机连接到mysql服务器的话。 GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%'IDENTIFIED BY 'mypassword' WI TH GRANT OPTION; 如果你想允许用户myuser从ip为192.168.1.6的主机连接到mysql服务器,并使用mypassword作 CommonDAO(公共/基础DAO) g21121 DAO 好久没有更新博客了,最近一段时间工作比较忙,所以请见谅,无论你是爱看呢还是爱看呢还是爱看呢,总之或许对你有些帮助。 DAO(Data Access Object)是一个数据访问(顾名思义就是与数据库打交道)接口,DAO一般在业 直言有讳 永夜-极光 感悟随笔 1.转载地址:http://blog.csdn.net/jasonblog/article/details/10813313 精华: “直言有讳”是阿里巴巴提倡的一种观念,而我在此之前并没有很深刻的认识。为什么呢?就好比是读书时候做阅读理解,我喜欢我自己的解读,并不喜欢老师给的意思。在这里也是。我自己坚持的原则是互相尊重,我觉得阿里巴巴很多价值观其实是基本的做人 安装CentOS 7 和Win 7后,Win7 引导丢失 随便小屋 centos 一般安装双系统的顺序是先装Win7,然后在安装CentOS,这样CentOS可以引导WIN 7启动。但安装CentOS7后,却找不到Win7 的引导,稍微修改一点东西即可。 一、首先具有root 的权限。 即进入Terminal后输入命令su,然后输入密码即可 二、利用vim编辑器打开/boot/grub2/grub.cfg文件进行修改 v Oracle备份与恢复案例 aijuans oracle Oracle备份与恢复案例 一. 理解什么是数据库恢复当我们使用一个数据库时,总希望数据库的内容是可靠的、正确的,但由于计算机系统的故障(硬件故障、软件故障、网络故障、进程故障和系统故障)影响数据库系统的操作,影响数据库中数据的正确性,甚至破坏数据库,使数据库中全部或部分数据丢失。因此当发生上述故障后,希望能重构这个完整的数据库,该处理称为数据库恢复。恢复过程大致可以分为复原(Restore)与 JavaEE开源快速开发平台G4Studio v5.0发布 無為子 我非常高兴地宣布,今天我们最新的JavaEE开源快速开发平台G4Studio_V5.0版本已经正式发布。 访问G4Studio网站 http://www.g4it.org 2013-04-06 发布G4Studio_V5.0版本 功能新增 (1). 新增了调用Oracle存储过程返回游标,并将游标映射为Java List集合对象的标 Oracle显示根据高考分数模拟录取 百合不是茶 PL/SQL编程oracle例子模拟高考录取学习交流 题目要求: 1,创建student表和result表 2,pl/sql对学生的成绩数据进行处理 3,处理的逻辑是根据每门专业课的最低分线和总分的最低分数线自动的将录取和落选 1,创建student表,和result表 学生信息表; create table student( student_id number primary key,--学生id 优秀的领导与差劲的领导 bijian1013 领导管理团队 责任 优秀的领导:优秀的领导总是对他所负责的项目担负起责任。如果项目不幸失败了,那么他知道该受责备的人是他自己,并且敢于承认错误。 差劲的领导:差劲的领导觉得这不是他的问题,因此他会想方设法证明是他的团队不行,或是将责任归咎于团队中他不喜欢的那几个成员身上。 努力工作 优秀的领导:团队领导应该是团队成员的榜样。至少,他应该与团队中的其他成员一样努力工作。这仅仅因为他 js函数在浏览器下的兼容 Bill_chen jquery浏览器IEDWRext 做前端开发的工程师,少不了要用FF进行测试,纯js函数在不同浏览器下,名称也可能不同。对于IE6和FF,取得下一结点的函数就不尽相同: IE6:node.nextSibling,对于FF是不能识别的; FF:node.nextElementSibling,对于IE是不能识别的; 兼容解决方式:var Div = node.nextSibl 【JVM四】老年代垃圾回收:吞吐量垃圾收集器(Throughput GC) bit1129 垃圾回收 吞吐量与用户线程暂停时间 衡量垃圾回收算法优劣的指标有两个: 吞吐量越高,则算法越好 暂停时间越短,则算法越好 首先说明吞吐量和暂停时间的含义。 垃圾回收时,JVM会启动几个特定的GC线程来完成垃圾回收的任务,这些GC线程与应用的用户线程产生竞争关系,共同竞争处理器资源以及CPU的执行时间。GC线程不会对用户带来的任何价值,因此,好的GC应该占 J2EE监听器和过滤器基础 白糖_ J2EE Servlet程序由Servlet,Filter和Listener组成,其中监听器用来监听Servlet容器上下文。 监听器通常分三类:基于Servlet上下文的ServletContex监听,基于会话的HttpSession监听和基于请求的ServletRequest监听。 ServletContex监听器 ServletContex又叫application 博弈AngularJS讲义(16) - 提供者 boyitech jsAngularJSapiAngularProvider Angular框架提供了强大的依赖注入机制,这一切都是有注入器(injector)完成. 注入器会自动实例化服务组件和符合Angular API规则的特殊对象,例如控制器,指令,过滤器动画等。 那注入器怎么知道如何去创建这些特殊的对象呢? Angular提供了5种方式让注入器创建对象,其中最基础的方式就是提供者(provider), 其余四种方式(Value, Fac java-写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 bylijinnan java public class CommonSubSequence { /** * 题目:写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 * 写一个版本算法复杂度O(N^2)和一个O(N) 。 * * O(N^2):对于a中的每个字符,遍历b中的每个字符,如果相同,则拷贝到新字符串中。 * O( sqlserver 2000 无法验证产品密钥 Chen.H sqlwindowsSQL ServerMicrosoft 在 Service Pack 4 (SP 4), 是运行 Microsoft Windows Server 2003、 Microsoft Windows Storage Server 2003 或 Microsoft Windows 2000 服务器上您尝试安装 Microsoft SQL Server 2000 通过卷许可协议 (VLA) 媒体。 这样做, 收到以下错误信息CD KEY的 SQ [新概念武器]气象战争 comsci 气象战争的发动者必须是拥有发射深空航天器能力的国家或者组织.... 原因如下: 地球上的气候变化和大气层中的云层涡旋场有密切的关系,而维持一个在大气层某个层次 oracle 中 rollup、cube、grouping 使用详解 daizj oraclegroupingrollupcube oracle 中 rollup、cube、grouping 使用详解 -- 使用oracle 样例表演示 转自namesliu -- 使用oracle 的样列库,演示 rollup, cube, grouping 的用法与使用场景 --- ROLLUP , 为了理解分组的成员数量,我增加了 分组的计数 COUNT(SAL) 技术资料汇总分享 Dead_knight 技术资料汇总 分享 本人汇总的技术资料,分享出来,希望对大家有用。 http://pan.baidu.com/s/1jGr56uE 资料主要包含: Workflow->工作流相关理论、框架(OSWorkflow、JBPM、Activiti、fireflow...) Security->java安全相关资料(SSL、SSO、SpringSecurity、Shiro、JAAS...) Ser 初一下学期难记忆单词背诵第一课 dcj3sjt126com englishword could 能够 minute 分钟 Tuesday 星期二 February 二月 eighteenth 第十八 listen 听 careful 小心的,仔细的 short 短的 heavy 重的 empty 空的 certainly 当然 carry 携带;搬运 tape 磁带 basket 蓝子 bottle 瓶 juice 汁,果汁 head 头;头部 截取视图的图片, 然后分享出去 dcj3sjt126com OSObjective-C OS 7 has a new method that allows you to draw a view hierarchy into the current graphics context. This can be used to get an UIImage very fast. I implemented a category method on UIView to get the vi MySql重置密码 fanxiaolong MySql重置密码 方法一: 在my.ini的[mysqld]字段加入: skip-grant-tables 重启mysql服务,这时的mysql不需要密码即可登录数据库 然后进入mysql mysql>use mysql; mysql>更新 user set password=password('新密码') WHERE User='root'; mysq Ehcache(03)——Ehcache中储存缓存的方式 234390216 ehcacheMemoryStoreDiskStore存储驱除策略 Ehcache中储存缓存的方式 目录 1 堆内存(MemoryStore) 1.1 指定可用内存 1.2 驱除策略 1.3 元素过期 2 &nbs spring mvc中的@propertysource jackyrong spring mvc 在spring mvc中,在配置文件中的东西,可以在java代码中通过注解进行读取了: @PropertySource 在spring 3.1中开始引入 比如有配置文件 config.properties mongodb.url=1.2.3.4 mongodb.db=hello 则代码中 @PropertySource(& 重学单例模式 lanqiu17 单例Singleton模式 最近在重新学习设计模式,感觉对模式理解更加深刻。觉得有必要记下来。 第一个学的就是单例模式,单例模式估计是最好理解的模式了。它的作用就是防止外部创建实例,保证只有一个实例。 单例模式的常用实现方式有两种,就人们熟知的饱汉式与饥汉式,具体就不多说了。这里说下其他的实现方式 静态内部类方式: package test.pattern.singleton.statics; publ .NET开源核心运行时,且行且珍惜 netcome java.net开源 背景 2014年11月12日,ASP.NET之父、微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣布,微软将开源全部.NET核心运行时,并将.NET 扩展为可在 Linux 和 Mac OS 平台上运行。.NET核心运行时将基于MIT开源许可协议发布,其中将包括执行.NET代码所需的一切项目——CLR、JIT编译器、垃圾收集器(GC)和核心 使用oscahe缓存技术减少与数据库的频繁交互 Everyday都不同 Web高并发oscahe缓存 此前一直不知道缓存的具体实现,只知道是把数据存储在内存中,以便下次直接从内存中读取。对于缓存的使用也没有概念,觉得缓存技术是一个比较”神秘陌生“的领域。但最近要用到缓存技术,发现还是很有必要一探究竟的。 缓存技术使用背景:一般来说,对于web项目,如果我们要什么数据直接jdbc查库好了,但是在遇到高并发的情形下,不可能每一次都是去查数据库,因为这样在高并发的情形下显得不太合理—— Spring+Mybatis 手动控制事务 toknowme mybatis @Override public boolean testDelete(String jobCode) throws Exception { boolean flag = false; &nbs 菜鸟级的android程序员面试时候需要掌握的知识点 xp9802 android 熟悉Android开发架构和API调用 掌握APP适应不同型号手机屏幕开发技巧 熟悉Android下的数据存储 熟练Android Debug Bridge Tool 熟练Eclipse/ADT及相关工具 熟悉Android框架原理及Activity生命周期 熟练进行Android UI布局 熟练使用SQLite数据库; 熟悉Android下网络通信机制,S 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
公平锁实现的Lua脚本
-- 1.死循环清除过期key while true do -- 获取头节点 local firstThreadId2 = redis.call('lindex', KEYS[2], 0); -- 首次获取必空跳出循环 if firstThreadId2 == false then break; end; -- 清除过期key local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2)); if timeout <= tonumber(ARGV[4]) then redis.call('zrem', KEYS[3], firstThreadId2); redis.call('lpop', KEYS[2]); else break; end; end; -- 2.不存在该锁 && (不存在线程等待队列 || 存在线程等待队列而且第一个节点就是此线程ID),加锁部分主要逻辑 if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then -- 弹出队列中线程id元素,删除Zset中该线程id对应的元素 redis.call('lpop', KEYS[2]); redis.call('zrem', KEYS[3], ARGV[2]); local keys = redis.call('zrange', KEYS[3], 0, -1); -- 遍历zSet所有key,将key的超时时间(score) - 当前时间ms for i = 1, #keys, 1 do redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]); end; -- 加锁设置锁过期时间 redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 3.线程存在,重入判断 if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then redis.call('hincrby', KEYS[1], ARGV[2],1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 4.返回当前线程剩余存活时间 local timeout = redis.call('zscore', KEYS[3], ARGV[2]); if timeout ~= false then -- 过期时间timeout的值在下方设置,此处的减法算出的依旧是当前线程的ttl return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]); end; -- 5.尾节点剩余存活时间 local lastThreadId = redis.call('lindex', KEYS[2], -1); local ttl; -- 尾节点不空 && 尾节点非当前线程 if lastThreadId ~= false and lastThreadId ~= ARGV[2] then -- 计算队尾节点剩余存活时间 ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]); else -- 获取lock_name剩余存活时间 ttl = redis.call('pttl', KEYS[1]); end; -- 6.末尾排队 -- zSet 超时时间(score),尾节点ttl + 当前时间 + 5000ms + 当前时间,无则新增,有则更新 -- 线程id放入队列尾部排队,无则插入,有则不再插入 local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then redis.call('rpush', KEYS[2], ARGV[2]); end; return ttl;
通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。
其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤
1.队列清理
保证队列中只有未过期的等待线程
2.首次加锁
hset加锁,pexpire过期时间
3.重入判断
此处同可重入锁lua
4.返回ttl
5.计算尾节点ttl
初始值为锁的剩余过期时间
6.末尾排队
ttl + 2 * currentTime + waitTime是score的默认值计算公式
如果模拟以下顺序,就会明了redisson公平锁整个加锁流程
假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20
t1:当线程1初次获取锁
1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束
t2:线程2尝试获取锁(线程1未释放锁)
1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10
t3:线程3尝试获取锁(线程1未释放锁)
1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20
如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。
而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取)
10:00:30 线程2尝试获取锁(线程1已释放锁)
1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束
排队结构如图:
公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。
由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。
并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。
Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。
Redisson也提供了多机情况下的联锁MultiLock:
https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock
和官方推荐的红锁RedLock:
https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock
所以,当你真的需要分布式锁时,不妨先来Redisson里找找。