什么是Redisson?—— Redisson Wiki
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命令封装好,Lettuce则进一步有了更丰富的Api,也支持集群等模式。但是两者也都点到为止,只给了你操作Redis数据库的脚手架,而Redisson则是基于Redis、Lua和Netty建立起了成熟的分布式解决方案,甚至redis官方都推荐的一种工具集。
分布式锁怎么实现?
分布式锁是并发业务下的刚需,虽然实现五花八门:ZooKeeper有Znode顺序节点,数据库有表级锁和乐/悲观锁,Redis有setNx,但是殊途同归,最终还是要回到互斥上来,本篇介绍Redisson,那就以redis为例。
怎么写一个简单的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脚本是什么?
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都很丝滑,因为他们都是可重入锁,一个线程多次拿锁也不会死锁,我们需要可重入。 怎么保证可重入? 重入就是,同一个线程多次获取同一把锁是允许的,不会造成死锁,这一点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命令删除 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.计数器的加减 当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减 判断一个redis key是否存在,可以用exists,而判断一个hash的key是否存在,可以用hexists 而redis也有hash自增的命令hincrby 每次自增1时 hincrby lockname1 threadId 1,自减1时 hincrby lockname1 threadId -1 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实现分布式锁 * @date 2021/2/6 10:51 下午 **/ @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就有这把你要的锁。 三、Redisson分布式锁 号称简单的Redisson分布式锁的使用姿势是什么? 1.依赖 org.redisson redisson 3.13.6 org.redisson redisson-spring-boot-starter 3.13.6 复制代码 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.启用分布式锁 @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 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功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。 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模型(参考Netty中的异步编程),Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解 在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。 这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务。 这段定时任务的就是watchDog的核心。 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实现的基本思路。 3.流程概括 通过整体的介绍,流程简单概括: A、B线程争抢一把锁,A获取到后,B阻塞 B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息 A操作完成释放了锁,B线程收到订阅消息通知 B被唤醒开始继续抢锁,拿到锁 详细加锁解锁流程总结如下图: 五、公平锁 以上介绍的可重入锁是非公平锁,Redisson还基于Redis的队列(List)和ZSet实现了公平锁 公平的定义是什么? 公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少 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。 保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则 为什么JUC以默认非公平锁呢? 因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。 可见公平的代价是性能与吞吐量。 Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。 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; 复制代码 1.公平锁加锁步骤 通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。 其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤 队列清理 保证队列中只有未过期的等待线程 首次加锁 hset加锁,pexpire过期时间 重入判断 此处同可重入锁lua 返回ttl 计算尾节点ttl 初始值为锁的剩余过期时间 末尾排队 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通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 六、总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现,Redisson也提供了多机情况下的联锁(MultiLock)和官方推荐的红锁(RedLock),下一章再详细介绍。 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(redis,java,redis,开发语言) SpringBoot整合DeepSeek技术指南(2025版) hjy1821 AI人工智能 SpringBoot整合DeepSeek技术指南(2025版)环境准备com.deepseekdeepseek-java-sdk2.5.0org.springframework.bootspring-boot-starter-webflux配置中心设置#application.ymldeepseek:api:base-url:https://api.deepseek.com/v2token:${D Oracle 数据库基础入门(五):限制查询与范式三约定深度解析 Aphelios380 Oracle数据库oracle学习java 在Oracle数据库的学习进程中,限制查询与范式三约定是两个极为重要的概念。限制查询帮助我们精准获取特定范围的数据,而范式三约定则为数据库设计提供了科学的指导框架。对于Java全栈开发者而言,掌握这些知识不仅有助于高效地从数据库中提取数据,更能设计出结构合理、性能优良的数据库,为构建强大的应用系统奠定坚实基础。目录一、Oracle限制查询(一)与MySQL限制查询的对比(二)伪列的奥秘(三)限制查 7-11 sdut-String-5 图书价格汇总(II) (10 分) 灯火穿透了 PTAJava题java 假设图书馆中图书信息的格式为:Java程序设计:34;Web程序设计:56;JSP程序设计:20按要求输出每本图书的名称及价格,计算所有图书的总价格并输出。输入格式:读入一行图书信息。如:Java程序设计:34;Web程序设计:56;JSP程序设计:20提示:每本书的价格是整数,价格与下一本书的名字之间有一个中文;价格前可能有空格,可能没有。输出格式:分别输出每本图书的名称及价格,一本书占一行,形 三、Jvm内存分配 刘总Java Java虚拟机jvmjava开发语言 今天的博客主题Java虚拟机——》Jvm内存分配什么是JVM内存分配呢?就是当我们创建一个对象的时候,要在JVM内存空间里为这个对象分配一些空间,来存放对象的一些属性信息。对象创建的流程1)类加载检查在创建对象的过程中,也就是new一个对象的时候。首先检查new指令的参数在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如有没有,说明这个类没有被加载使用过 Java生成LRC纵向冗余校验 YunFeiDong Javajava开发语言ModbusASCII 纵向冗余校验(LongitudinalRedundancyCheck,简称:LRC)是通信中常用的一种校验形式,也称LRC校验或纵向校验;它是一种从纵向通道上的特定比特串产生校验比特的错误检测方法;通常Modbus协议ASCII模式采用LRC算法。1.生成LRC校验/***生成LRC校验值:**1)对需要校验的数据(2n个字符)两两组成一个16进制的数值求和;*2)将求和结果与256求模;*3)用 Java基础之JVM对象内存分配机制简介 aoneword JVM基础jvmjava开发语言 一对象内存分配1.1运行时数据区域1.2常见java应用启动JVM参数:-Xss:每个线程的栈大小(单位kb)-Xms:堆的初始大小,默认物理内存的1/64,示例:-Xms:4g-Xms:10m-Xmx:堆的最大可用大小,默认物理内存的1/4-Xmn:新生代大小-XX:NewRatio:默认2,表示新生代占老年代的1/2,占整个堆内存的1/3-XX:SurvivorRatio:默认8,表示一个su python文件修改后不生效_pycharm修改代码后第一次运行不生效解决 weixin_39959126 python文件修改后不生效 SVN搭建简单教程一.引言笔者曾经试图在网上搜索一篇关于SVN源代码服务器搭建方面的中文技术文章,可惜,所找到的,要么是不完整,要么就是对笔者没什么帮助的文章,TortoiseSvn的帮助文档固然强大,但因为是英文,...java零碎知识点1.字符串有整型的相互转换12Stringa=String.valueOf(2);//integertonumericstringinti=Integer.pa react 基础 理想和远方_在路上 react.jsjsx 一.什么是react?react是一种用于构建用户界面的javaScript库react主要用来写html,或构建web应用如果用mvc的角度来看,react仅仅是视图层(v),也就是只负责视图的渲染,并不是提供了完成的M和C的功能。二.react的特点1.声明式只需要描述html看起来是什么样,就跟写html一样。react负责渲染UI,并在数据变化是更新UI。constjsx=HelloRea java excel 导入 加校验_Java通过POI为Excel添加数据验证 夏至未至 javaexcel导入加校验 Stringpath="d:\\success.xlsx";StringsheetName="sheetlist";XSSFWorkbookwb=null;XSSFSheetsheetlist=null;FileinputFile=newFile(path);if(inputFile.exists()){wb=newXSSFWorkbook(newFileInputStream(path));}e SpringAI赋能Java开发打造智能应用 java技术小馆 javaAI编程 一、SpringAI是什么?为什么你需要它?想象一下,你的Java应用能够:理解自然语言自动生成代码智能分析数据提供个性化推荐这就是SpringAI带来的变革!它是Spring官方推出的AI集成框架,让你的Java应用轻松获得AI能力。传统开发vsSpringAI开发对比:能力传统开发SpringAI开发自然语言处理需要集成第三方SDK开箱即用开发效率手动实现复杂逻辑自动生成代码维护成本高低可扩展 IDEA编写JAVA的常用快捷键 峥嵘轻稠 intellij-ideajavaintellijidea (摘要:这是我平常使用IDEA的快捷键,希望能帮到和我一样刚入门的小白~~创作不易,希望能得到点赞关注支持嘿嘿嘿~~~)Ctrl+A:全选Ctrl+Z:撤销Ctrl+X:剪切Ctrl+C:复制Ctrl+V:粘贴Ctrl+Y:删除当前行Ctrl+D:复制当前行到下一行Ctrl+O:选择可重写的方法Ctrl+I:重写接口方法Ctrl+/:快速单行注释Ctrl+Shift+/:快速多行注释Ctrl+Al JAVA学习——DAY1 E卤蛋 JAVA学习java 几个cmd终端命令:cls——清屏cd——移至所在路径,后跟绝对路径or相对路径ip-config——查看本机ip信息java开发环境:JDK——java开发环境,包含:JRE(Java运行时环境),JVM(java虚拟机),以及各类开发库文件;安装目录:bin目录:两个重要——javac.exe(java编译工具),从.java文件编译为.class(字节码文件),java.exe(Java运行 JVM两种内存分配方式 小白,想脱白 jvm 1、指针碰撞如果jvm内存是规整的,就是一边是已使用的内存,另外一边是未使用的内存,中间是指针,jvm需要给新对象分配内存时,就会将指针往未使用的内存移动一段和新对象一样大小的距离,这样jvm就完成了内存分配。2、空闲列表如果jvm内存不是规整的,已使用的,未使用的内存,都是杂乱无章的,这个时候就不可能使用指针碰撞,Java虚拟机就会维护一个空闲列表,用来记录哪些内存是可用的,然后jvm给新对象分 JAVA多线程详解(超详细) m0_74823434 面试学习路线阿里巴巴资料职业发展javapython开发语言后端 目录一、线程简介1、进程、线程2、并发、并行、串行3、进程的三态二、线程实现1、继承Thread类2、实现Runnable接口3、实现Callable接口(不常用)三、线程常用方法1、线程的状态2、线程常用方法四、多线程1、守护(Deamon)线程2、多线程并发与同步3、死锁4、Lock(锁)5、线程协作6、线程池一、线程简介1、进程、线程程序:开发写的代码称之为程序。程序就是一堆代码,一组数据和 JMeter 不同协议测试最佳实践汇总 那片海还在吗 testjmeter测试工具 JMeter不同协议测试最佳实践汇总一、JMeter测试HTTPS(一)环境准备JMeter安装:从JMeter官方网站下载并解压JMeter到本地。Java环境:确保系统已安装Java运行环境(JRE)或开发环境(JDK),建议使用Java8及以上版本。(二)创建测试计划启动JMeter,默认有一个测试计划。右键点击测试计划,选择“添加”->“线程(用户)”->“线程组”,可设置线程数、循环次数 【React全解】React起手式 caihuayuan4 面试题汇总与解析springsqljava大数据 如何引入React从bootcdn引入React按顺序引入React然后引入ReactDOMCommonJSVSUMD规范JavaScript的模块定义和加载机制,降低了学习和使用各种框架的门槛,能够以一种统一的方式去定义和使用模块,提高开发效率,降低了应用维护成本CommonJSCommonJS是一种规范,NodeJS是这种规范的实现CommonJS模块是对象,是运行时加载,运行时才把模块挂载在 com.mysql.jdbc.Driver 和 com.mysql.cj.jdbc.Driver的区别 caihuayuan4 面试题汇总与解析springsqljava大数据课程设计 com.mysql.jdbc.Driver是mysql-connector-java5中的com.mysql.cj.jdbc.Driver是mysql-connector-java6中的所以在使用的时候一定要注意版本com.mysql.jdbc.DriverdriverClassName=com.mysql.jdbc.Driverurl=jdbc:mysql://localhost:3306/te Java学习——day14 blackA_ java学习开发语言 文章目录1.项目需求分析2.项目设计3.代码分析4.运行示例5.今日学习总结6.今日生词今日学习计划1.项目需求分析功能要求:(1)存储学生信息:使用HashMap存储学生信息(学号作为键,Student对象作为值)。(2)操作学生数据:添加学生(姓名、学号、成绩)。删除学生(按学号删除)。查询学生(按学号查询)。显示所有学生信息。(3)异常处理:防止重复添加(如果学号已存在,抛出异常)。查询/删 mac idea配置了八百次maven都不成功,mac source不生效 source ~/.bash_profile Alisa_wu666 ideamavensourcemaven 终端输入open~/.bash_profile,检查内容:exportPATH=/usr/bin:/usr/sbin:/bin:/sbinexportJAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/HomeexportM2_HOME=/Users/nanwu/mooc/apache-maven-3.5.3e 后端java的复习-常用API(个人笔记) 狴犴ys java基础后端技术栈巩固复习java 常用apiObjecttoString方法equals方法finalize方法clone()方法SystemString构造方法常用方法获取型判断型转换型StringBuffer与StringBuilder八种基本数据类型包装类八种包装类IntegerDate获取系统当前时间Date->StringString->DateCalendar(日历)介绍常用方法匿名内部类ObjecttoString方 秒杀业务中的库存扣减为什么不加分布式锁? 提前退休了-程序员阿飞 分布式 前言说到秒杀业务的库存扣减,就还是得先确认我们的扣减基本方案。秒杀场景的库存扣减方案一般的做法是,先在Redis中做扣减,然后发送一个MQ消息,消费者在接到消息之后做数据库中库存的真正扣减及业务逻辑操作。如何解决数据一致性问题:Redis中库存成功扣减了,但是后续发送MQ消息失败,或者后面的消费过程中消息丢了或者失败了等情况。就会导致Redis中的库存被扣减了,但是数据库库存没扣减,业务的实际操作 Kotlin/Java 实现 Html 转图片 zimoyin javakotlinhtml 1.Html2Imagegui.avahtml2image2.0.1StringHtmlTemplateStr="....";//HTMLHtmlParserhtmlParser=newHtmlParserImpl();htmlParser.loadHtml(HtmlTemplateStr);ImageRendererimageRenderer=newImageRendererImpl(htmlP 【赵渝强老师】监控Redis 数据库nosqlredis 对运行状态的Redis实例进行监控是运维管理中非常重要的内容,包括:监控Redis的内存、监控Redis的吞吐量、监控Redis的运行时信息和监控Redis的延时。通过Redis提供的监控命令便能非常方便地实现对各项指标的监控。一、监控Redis的内存视频讲解如下:https://www.bilibili.com/video/BV1mi28YXE14/?aid=113294358616...Red jQuery获取并解析API接口Json数据的方法 qhdzj87 前端(JSjQuery等)jqueryjsonjavascript json是API的常用数据交换格式,在Web开发中,除了后端程序,前端也可以借助JavaScript技术获取并解析API中的json数据,这里介绍一种通过jQuery的$get()函数获取并解析API接口Json数据的方法。其基本语法如下:$get(url,[data],[callback])在$get()函数中,包含了三个参数。其中:url:请求的地址;data:请求数据的列表;callback 如何设置HTTP请求中的参数? 数据小爬虫@ http网络协议网络 在Java中设置HTTP请求的参数是爬虫开发中的一个常见任务。这可以通过多种方式实现,具体取决于你使用的库。以下将展示如何使用ApacheHttpClient和Java11+的HttpClient设置HTTP请求中的参数。一、使用ApacheHttpClient设置请求参数(一)添加依赖如果你使用的是Maven,可以在pom.xml文件中添加以下依赖:org.apache.httpcomponen 如何使用Java爬虫处理API接口返回的JSON数据? 小爬虫程序猿 APIjavajson开发语言 处理API接口返回的JSON数据是Java爬虫开发中的一个常见任务。在Java中,有多个库可以帮助我们解析JSON数据,其中最流行的是Jackson和Gson。以下是使用这两个库处理JSON数据的基本步骤和示例代码。使用Jackson处理JSONJackson是一个功能强大的JSON处理库,它不仅可以将JSON字符串解析为Java对象,还可以将Java对象转换为JSON字符串。添加Jackson依 Spring Cache缓存注解深度解析 coder lei spring缓存java SpringCache缓存注解深度解析一、框架概述SpringCache是Spring框架提供的抽象缓存层,通过注解实现声明式缓存,与具体缓存实现(如Redis、Ehcache)解耦。核心接口CacheManager负责管理不同缓存,@EnableCaching开启注解驱动。二、核心注解详解1.@Cacheable作用:方法结果缓存,首次调用后缓存结果关键参数:@Cacheable(value=" 网络原理 初识[Java EE] 猿周LV JavaEE网络原理网络java-eejava 目录网络发展史独立模式网络互联局域网LAN1.基于网络直连2.基于集线器(Hub)组建3.基于交换机(Switch)组建4.基于交换机和路由器(Router)组建广域网WAN网络通信基础IP地址1.概念2.格式端口号1.概念2.格式认识协议1.概念2.作用3.协议分层3.1什么是协议分层3.2分层的作用3.2.1上层不需要了解下层的细节(封装)3.2.2灵活的调整/替换某层的协议4.五元组4.1源 Redis--单线程模型 04Koi. Redisredis数据库缓存 目录一、引言二、Redis单线程模型三、原因四、为什么redis是单线程模型,但他的速度这么快?五、总结一、引言本篇文章就Redis为什么是单线程模型做简单介绍。二、Redis单线程模型redis只使用一个线程,处理所有的命令请求,但是不是说redis服务器内部真的就只有一个线程,其实也有多个线程,这些线程在处理网络IO。假设同时有两个客户端向redis服务器发送了命令请求,但是redis还是会将 C++ vector::push_back和Java List.add的区别 da_kao_la JavaCppJavaCppvectorList C++vector::push_back和JavaList.add的区别对象赋值C++和Java在对象赋值方面区别较大,归根到底,还是因为C++和Java中对象存储和引用方式不同。C++中静态建立(即Objobj)的对象的内容(类属性)是存储在栈上的,变量obj直接持有对象本身;Java中建立的对象(Objobj=newObj())的内容(类属性)是存储在堆上的,位于栈上的变量obj只是持有对象的 集合框架 天子之骄 java数据结构集合框架 集合框架 集合框架可以理解为一个容器,该容器主要指映射(map)、集合(set)、数组(array)和列表(list)等抽象数据结构。 从本质上来说,Java集合框架的主要组成是用来操作对象的接口。不同接口描述不同的数据类型。 简单介绍: Collection接口是最基本的接口,它定义了List和Set,List又定义了LinkLi Table Driven(表驱动)方法实例 bijian1013 javaenumTable Driven表驱动 实例一: /** * 驾驶人年龄段 * 保险行业,会对驾驶人的年龄做年龄段的区分判断 * 驾驶人年龄段:01-[18,25);02-[25,30);03-[30-35);04-[35,40);05-[40,45);06-[45,50);07-[50-55);08-[55,+∞) */ public class AgePeriodTest { //if...el Jquery 总结 cuishikuan javajqueryAjaxWebjquery方法 1.$.trim方法用于移除字符串头部和尾部多余的空格。如:$.trim(' Hello ') // Hello2.$.contains方法返回一个布尔值,表示某个DOM元素(第二个参数)是否为另一个DOM元素(第一个参数)的下级元素。如:$.contains(document.documentElement, document.body); 3.$ 面向对象概念的提出 麦田的设计者 java面向对象面向过程 面向对象中,一切都是由对象展开的,组织代码,封装数据。 在台湾面向对象被翻译为了面向物件编程,这充分说明了,这种编程强调实体。 下面就结合编程语言的发展史,聊一聊面向过程和面向对象。 c语言由贝尔实 linux网口绑定 被触发 linux 刚在一台IBM Xserver服务器上装了RedHat Linux Enterprise AS 4,为了提高网络的可靠性配置双网卡绑定。 一、环境描述 我的RedHat Linux Enterprise AS 4安装双口的Intel千兆网卡,通过ifconfig -a命令看到eth0和eth1两张网卡。 二、双网卡绑定步骤: 2.1 修改/etc/sysconfig/network XML基础语法 肆无忌惮_ xml 一、什么是XML? XML全称是Extensible Markup Language,可扩展标记语言。很类似HTML。XML的目的是传输数据而非显示数据。XML的标签没有被预定义,你需要自行定义标签。XML被设计为具有自我描述性。是W3C的推荐标准。 二、为什么学习XML? 用来解决程序间数据传输的格式问题 做配置文件 充当小型数据库 三、XML与HTM 为网页添加自己喜欢的字体 知了ing 字体 秒表 css @font-face { font-family: miaobiao;//定义字体名字 font-style: normal; font-weight: 400; src: url('font/DS-DIGI-e.eot');//字体文件 } 使用: <label style="font-size:18px;font-famil redis范围查询应用-查找IP所在城市 矮蛋蛋 redis 原文地址: http://www.tuicool.com/articles/BrURbqV 需求 根据IP找到对应的城市 原来的解决方案 oracle表(ip_country): 查询IP对应的城市: 1.把a.b.c.d这样格式的IP转为一个数字,例如为把210.21.224.34转为3524648994 2. select city from ip_ 输入两个整数, 计算百分比 alleni123 java public static String getPercent(int x, int total){ double result=(x*1.0)/(total*1.0); System.out.println(result); DecimalFormat df1=new DecimalFormat("0.0000%"); 百合——————>怎么学习计算机语言 百合不是茶 java 移动开发 对于一个从没有接触过计算机语言的人来说,一上来就学面向对象,就算是心里上面接受的了,灵魂我觉得也应该是跟不上的,学不好是很正常的现象,计算机语言老师讲的再多,你在课堂上面跟着老师听的再多,我觉得你应该还是学不会的,最主要的原因是你根本没有想过该怎么来学习计算机编程语言,记得大一的时候金山网络公司在湖大招聘我们学校一个才来大学几天的被金山网络录取,一个刚到大学的就能够去和 linux下tomcat开机自启动 bijian1013 tomcat 方法一: 修改Tomcat/bin/startup.sh 为: export JAVA_HOME=/home/java1.6.0_27 export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:. export PATH=$JAVA_HOME/bin:$PATH export CATALINA_H spring aop实例 bijian1013 javaspringAOP 1.AdviceMethods.java package com.bijian.study.spring.aop.schema; public class AdviceMethods { public void preGreeting() { System.out.println("--how are you!--"); } } 2.beans.x [Gson八]GsonBuilder序列化和反序列化选项enableComplexMapKeySerialization bit1129 serialization enableComplexMapKeySerialization配置项的含义 Gson在序列化Map时,默认情况下,是调用Key的toString方法得到它的JSON字符串的Key,对于简单类型和字符串类型,这没有问题,但是对于复杂数据对象,如果对象没有覆写toString方法,那么默认的toString方法将得到这个对象的Hash地址。 GsonBuilder用于 【Spark九十一】Spark Streaming整合Kafka一些值得关注的问题 bit1129 Stream 包括Spark Streaming在内的实时计算数据可靠性指的是三种级别: 1. At most once,数据最多只能接受一次,有可能接收不到 2. At least once, 数据至少接受一次,有可能重复接收 3. Exactly once 数据保证被处理并且只被处理一次, 具体的多读几遍http://spark.apache.org/docs/lates shell脚本批量检测端口是否被占用脚本 ronin47 #!/bin/bash cat ports |while read line do#nc -z -w 10 $line nc -z -w 2 $line 58422>/dev/null2>&1if[ $?-eq 0]then echo $line:ok else echo $line:fail fi done 这里的ports 既可以是文件 java-2.设计包含min函数的栈 bylijinnan java 具体思路参见:http://zhedahht.blog.163.com/blog/static/25411174200712895228171/ import java.util.ArrayList; import java.util.List; public class MinStack { //maybe we can use origin array rathe Netty源码学习-ChannelHandler bylijinnan javanetty 一般来说,“有状态”的ChannelHandler不应该是“共享”的,“无状态”的ChannelHandler则可“共享” 例如ObjectEncoder是“共享”的, 但 ObjectDecoder 不是 因为每一次调用decode方法时,可能数据未接收完全(incomplete), 它与上一次decode时接收到的数据“累计”起来才有可能是完整的数据,是“有状态”的 p java生成随机数 cngolon java 方法一: /** * 生成随机数 * @author cngolon@126.com * @return */ public synchronized static String getChargeSequenceNum(String pre){ StringBuffer sequenceNum = new StringBuffer(); Date dateTime = new D POI读写海量数据 ctrain 海量数据 import java.io.FileOutputStream; import java.io.OutputStream; import org.apache.poi.xssf.streaming.SXSSFRow; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming mysql 日期格式化date_format详细使用 daizj mysqldate_format日期格式转换日期格式化 日期转换函数的详细使用说明 DATE_FORMAT(date,format) Formats the date value according to the format string. The following specifiers may be used in the format string. The&n 一个程序员分享8年的开发经验 dcj3sjt126com 程序员 在中国有很多人都认为IT行为是吃青春饭的,如果过了30岁就很难有机会再发展下去!其实现实并不是这样子的,在下从事.NET及JAVA方面的开发的也有8年的时间了,在这里在下想凭借自己的亲身经历,与大家一起探讨一下。 明确入行的目的 很多人干IT这一行都冲着“收入高”这一点的,因为只要学会一点HTML, DIV+CSS,要做一个页面开发人员并不是一件难事,而且做一个页面开发人员更容 android欢迎界面淡入淡出效果 dcj3sjt126com android 很多Android应用一开始都会有一个欢迎界面,淡入淡出效果也是用得非常多的,下面来实现一下。 主要代码如下: package com.myaibang.activity; import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.os.CountDown linux 复习笔记之常见压缩命令 eksliang tar解压linux系统常见压缩命令linux压缩命令tar压缩 转载请出自出处:http://eksliang.iteye.com/blog/2109693 linux中常见压缩文件的拓展名 *.gz gzip程序压缩的文件 *.bz2 bzip程序压缩的文件 *.tar tar程序打包的数据,没有经过压缩 *.tar.gz tar程序打包后,并经过gzip程序压缩 *.tar.bz2 tar程序打包后,并经过bzip程序压缩 *.zi Android 应用程序发送shell命令 gqdy365 android 项目中需要直接在APP中通过发送shell指令来控制lcd灯,其实按理说应该是方案公司在调好lcd灯驱动之后直接通过service送接口上来给APP,APP调用就可以控制了,这是正规流程,但我们项目的方案商用的mtk方案,方案公司又没人会改,只调好了驱动,让应用程序自己实现灯的控制,这不蛋疼嘛!!!! 发就发吧! 一、关于shell指令: 我们知道,shell指令是Linux里面带的 java 无损读取文本文件 hw1287789687 读取文件无损读取读取文本文件charset java 如何无损读取文本文件呢? 以下是有损的 @Deprecated public static String getFullContent(File file, String charset) { BufferedReader reader = null; if (!file.exists()) { System.out.println("getFull Firebase 相关文章索引 justjavac firebase Awesome Firebase 最近谷歌收购Firebase的新闻又将Firebase拉入了人们的视野,于是我做了这个 github 项目。 Firebase 是一个数据同步的云服务,不同于 Dropbox 的「文件」,Firebase 同步的是「数据」,服务对象是网站开发者,帮助他们开发具有「实时」(Real-Time)特性的应用。 开发者只需引用一个 API 库文件就可以使用标准 RE C++学习重点 lx.asymmetric C++笔记 1.c++面向对象的三个特性:封装性,继承性以及多态性。 2.标识符的命名规则:由字母和下划线开头,同时由字母、数字或下划线组成;不能与系统关键字重名。 3.c++语言常量包括整型常量、浮点型常量、布尔常量、字符型常量和字符串性常量。 4.运算符按其功能开以分为六类:算术运算符、位运算符、关系运算符、逻辑运算符、赋值运算符和条件运算符。 &n java bean和xml相互转换 q821424508 javabeanxmlxml和bean转换java bean和xml转换 这几天在做微信公众号 做的过程中想找个java bean转xml的工具,找了几个用着不知道是配置不好还是怎么回事,都会有一些问题, 然后脑子一热谢了一个javabean和xml的转换的工具里,自己用着还行,虽然有一些约束吧 , 还是贴出来记录一下 顺便你提一下下,这个转换工具支持属性为集合、数组和非基本属性的对象。 packag C 语言初级 位运算 1140566087 位运算c 第十章 位运算 1、位运算对象只能是整形或字符型数据,在VC6.0中int型数据占4个字节 2、位运算符: 运算符 作用 ~ 按位求反 << 左移 >> 右移 & 按位与 ^ 按位异或 | 按位或 他们的优先级从高到低; 3、位运算符的运算功能: a、按位取反: ~01001101 = 101 14点睛Spring4.1-脚本编程 wiselyman spring4 14.1 Scripting脚本编程 脚本语言和java这类静态的语言的主要区别是:脚本语言无需编译,源码直接可运行; 如果我们经常需要修改的某些代码,每一次我们至少要进行编译,打包,重新部署的操作,步骤相当麻烦; 如果我们的应用不允许重启,这在现实的情况中也是很常见的; 在spring中使用脚本编程给上述的应用场景提供了解决方案,即动态加载bean; spring支持脚本 按字母分类: 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才能释放
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程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命令删除
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命令删除
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.计数器的加减
当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减
判断一个redis key是否存在,可以用exists,而判断一个hash的key是否存在,可以用hexists
exists
hexists
而redis也有hash自增的命令hincrby
hincrby
每次自增1时 hincrby lockname1 threadId 1,自减1时 hincrby lockname1 threadId -1
hincrby lockname1 threadId 1
hincrby lockname1 threadId -1
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实现分布式锁 * @date 2021/2/6 10:51 下午 **/ @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就有这把你要的锁。 三、Redisson分布式锁 号称简单的Redisson分布式锁的使用姿势是什么? 1.依赖 org.redisson redisson 3.13.6 org.redisson redisson-spring-boot-starter 3.13.6 复制代码 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.启用分布式锁 @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 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功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。 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模型(参考Netty中的异步编程),Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解 在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。 这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务。 这段定时任务的就是watchDog的核心。 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实现的基本思路。 3.流程概括 通过整体的介绍,流程简单概括: A、B线程争抢一把锁,A获取到后,B阻塞 B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息 A操作完成释放了锁,B线程收到订阅消息通知 B被唤醒开始继续抢锁,拿到锁 详细加锁解锁流程总结如下图: 五、公平锁 以上介绍的可重入锁是非公平锁,Redisson还基于Redis的队列(List)和ZSet实现了公平锁 公平的定义是什么? 公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少 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。 保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则 为什么JUC以默认非公平锁呢? 因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。 可见公平的代价是性能与吞吐量。 Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。 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; 复制代码 1.公平锁加锁步骤 通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。 其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤 队列清理 保证队列中只有未过期的等待线程 首次加锁 hset加锁,pexpire过期时间 重入判断 此处同可重入锁lua 返回ttl 计算尾节点ttl 初始值为锁的剩余过期时间 末尾排队 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通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 六、总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现,Redisson也提供了多机情况下的联锁(MultiLock)和官方推荐的红锁(RedLock),下一章再详细介绍。 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(redis,java,redis,开发语言) SpringBoot整合DeepSeek技术指南(2025版) hjy1821 AI人工智能 SpringBoot整合DeepSeek技术指南(2025版)环境准备com.deepseekdeepseek-java-sdk2.5.0org.springframework.bootspring-boot-starter-webflux配置中心设置#application.ymldeepseek:api:base-url:https://api.deepseek.com/v2token:${D Oracle 数据库基础入门(五):限制查询与范式三约定深度解析 Aphelios380 Oracle数据库oracle学习java 在Oracle数据库的学习进程中,限制查询与范式三约定是两个极为重要的概念。限制查询帮助我们精准获取特定范围的数据,而范式三约定则为数据库设计提供了科学的指导框架。对于Java全栈开发者而言,掌握这些知识不仅有助于高效地从数据库中提取数据,更能设计出结构合理、性能优良的数据库,为构建强大的应用系统奠定坚实基础。目录一、Oracle限制查询(一)与MySQL限制查询的对比(二)伪列的奥秘(三)限制查 7-11 sdut-String-5 图书价格汇总(II) (10 分) 灯火穿透了 PTAJava题java 假设图书馆中图书信息的格式为:Java程序设计:34;Web程序设计:56;JSP程序设计:20按要求输出每本图书的名称及价格,计算所有图书的总价格并输出。输入格式:读入一行图书信息。如:Java程序设计:34;Web程序设计:56;JSP程序设计:20提示:每本书的价格是整数,价格与下一本书的名字之间有一个中文;价格前可能有空格,可能没有。输出格式:分别输出每本图书的名称及价格,一本书占一行,形 三、Jvm内存分配 刘总Java Java虚拟机jvmjava开发语言 今天的博客主题Java虚拟机——》Jvm内存分配什么是JVM内存分配呢?就是当我们创建一个对象的时候,要在JVM内存空间里为这个对象分配一些空间,来存放对象的一些属性信息。对象创建的流程1)类加载检查在创建对象的过程中,也就是new一个对象的时候。首先检查new指令的参数在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如有没有,说明这个类没有被加载使用过 Java生成LRC纵向冗余校验 YunFeiDong Javajava开发语言ModbusASCII 纵向冗余校验(LongitudinalRedundancyCheck,简称:LRC)是通信中常用的一种校验形式,也称LRC校验或纵向校验;它是一种从纵向通道上的特定比特串产生校验比特的错误检测方法;通常Modbus协议ASCII模式采用LRC算法。1.生成LRC校验/***生成LRC校验值:**1)对需要校验的数据(2n个字符)两两组成一个16进制的数值求和;*2)将求和结果与256求模;*3)用 Java基础之JVM对象内存分配机制简介 aoneword JVM基础jvmjava开发语言 一对象内存分配1.1运行时数据区域1.2常见java应用启动JVM参数:-Xss:每个线程的栈大小(单位kb)-Xms:堆的初始大小,默认物理内存的1/64,示例:-Xms:4g-Xms:10m-Xmx:堆的最大可用大小,默认物理内存的1/4-Xmn:新生代大小-XX:NewRatio:默认2,表示新生代占老年代的1/2,占整个堆内存的1/3-XX:SurvivorRatio:默认8,表示一个su python文件修改后不生效_pycharm修改代码后第一次运行不生效解决 weixin_39959126 python文件修改后不生效 SVN搭建简单教程一.引言笔者曾经试图在网上搜索一篇关于SVN源代码服务器搭建方面的中文技术文章,可惜,所找到的,要么是不完整,要么就是对笔者没什么帮助的文章,TortoiseSvn的帮助文档固然强大,但因为是英文,...java零碎知识点1.字符串有整型的相互转换12Stringa=String.valueOf(2);//integertonumericstringinti=Integer.pa react 基础 理想和远方_在路上 react.jsjsx 一.什么是react?react是一种用于构建用户界面的javaScript库react主要用来写html,或构建web应用如果用mvc的角度来看,react仅仅是视图层(v),也就是只负责视图的渲染,并不是提供了完成的M和C的功能。二.react的特点1.声明式只需要描述html看起来是什么样,就跟写html一样。react负责渲染UI,并在数据变化是更新UI。constjsx=HelloRea java excel 导入 加校验_Java通过POI为Excel添加数据验证 夏至未至 javaexcel导入加校验 Stringpath="d:\\success.xlsx";StringsheetName="sheetlist";XSSFWorkbookwb=null;XSSFSheetsheetlist=null;FileinputFile=newFile(path);if(inputFile.exists()){wb=newXSSFWorkbook(newFileInputStream(path));}e SpringAI赋能Java开发打造智能应用 java技术小馆 javaAI编程 一、SpringAI是什么?为什么你需要它?想象一下,你的Java应用能够:理解自然语言自动生成代码智能分析数据提供个性化推荐这就是SpringAI带来的变革!它是Spring官方推出的AI集成框架,让你的Java应用轻松获得AI能力。传统开发vsSpringAI开发对比:能力传统开发SpringAI开发自然语言处理需要集成第三方SDK开箱即用开发效率手动实现复杂逻辑自动生成代码维护成本高低可扩展 IDEA编写JAVA的常用快捷键 峥嵘轻稠 intellij-ideajavaintellijidea (摘要:这是我平常使用IDEA的快捷键,希望能帮到和我一样刚入门的小白~~创作不易,希望能得到点赞关注支持嘿嘿嘿~~~)Ctrl+A:全选Ctrl+Z:撤销Ctrl+X:剪切Ctrl+C:复制Ctrl+V:粘贴Ctrl+Y:删除当前行Ctrl+D:复制当前行到下一行Ctrl+O:选择可重写的方法Ctrl+I:重写接口方法Ctrl+/:快速单行注释Ctrl+Shift+/:快速多行注释Ctrl+Al JAVA学习——DAY1 E卤蛋 JAVA学习java 几个cmd终端命令:cls——清屏cd——移至所在路径,后跟绝对路径or相对路径ip-config——查看本机ip信息java开发环境:JDK——java开发环境,包含:JRE(Java运行时环境),JVM(java虚拟机),以及各类开发库文件;安装目录:bin目录:两个重要——javac.exe(java编译工具),从.java文件编译为.class(字节码文件),java.exe(Java运行 JVM两种内存分配方式 小白,想脱白 jvm 1、指针碰撞如果jvm内存是规整的,就是一边是已使用的内存,另外一边是未使用的内存,中间是指针,jvm需要给新对象分配内存时,就会将指针往未使用的内存移动一段和新对象一样大小的距离,这样jvm就完成了内存分配。2、空闲列表如果jvm内存不是规整的,已使用的,未使用的内存,都是杂乱无章的,这个时候就不可能使用指针碰撞,Java虚拟机就会维护一个空闲列表,用来记录哪些内存是可用的,然后jvm给新对象分 JAVA多线程详解(超详细) m0_74823434 面试学习路线阿里巴巴资料职业发展javapython开发语言后端 目录一、线程简介1、进程、线程2、并发、并行、串行3、进程的三态二、线程实现1、继承Thread类2、实现Runnable接口3、实现Callable接口(不常用)三、线程常用方法1、线程的状态2、线程常用方法四、多线程1、守护(Deamon)线程2、多线程并发与同步3、死锁4、Lock(锁)5、线程协作6、线程池一、线程简介1、进程、线程程序:开发写的代码称之为程序。程序就是一堆代码,一组数据和 JMeter 不同协议测试最佳实践汇总 那片海还在吗 testjmeter测试工具 JMeter不同协议测试最佳实践汇总一、JMeter测试HTTPS(一)环境准备JMeter安装:从JMeter官方网站下载并解压JMeter到本地。Java环境:确保系统已安装Java运行环境(JRE)或开发环境(JDK),建议使用Java8及以上版本。(二)创建测试计划启动JMeter,默认有一个测试计划。右键点击测试计划,选择“添加”->“线程(用户)”->“线程组”,可设置线程数、循环次数 【React全解】React起手式 caihuayuan4 面试题汇总与解析springsqljava大数据 如何引入React从bootcdn引入React按顺序引入React然后引入ReactDOMCommonJSVSUMD规范JavaScript的模块定义和加载机制,降低了学习和使用各种框架的门槛,能够以一种统一的方式去定义和使用模块,提高开发效率,降低了应用维护成本CommonJSCommonJS是一种规范,NodeJS是这种规范的实现CommonJS模块是对象,是运行时加载,运行时才把模块挂载在 com.mysql.jdbc.Driver 和 com.mysql.cj.jdbc.Driver的区别 caihuayuan4 面试题汇总与解析springsqljava大数据课程设计 com.mysql.jdbc.Driver是mysql-connector-java5中的com.mysql.cj.jdbc.Driver是mysql-connector-java6中的所以在使用的时候一定要注意版本com.mysql.jdbc.DriverdriverClassName=com.mysql.jdbc.Driverurl=jdbc:mysql://localhost:3306/te Java学习——day14 blackA_ java学习开发语言 文章目录1.项目需求分析2.项目设计3.代码分析4.运行示例5.今日学习总结6.今日生词今日学习计划1.项目需求分析功能要求:(1)存储学生信息:使用HashMap存储学生信息(学号作为键,Student对象作为值)。(2)操作学生数据:添加学生(姓名、学号、成绩)。删除学生(按学号删除)。查询学生(按学号查询)。显示所有学生信息。(3)异常处理:防止重复添加(如果学号已存在,抛出异常)。查询/删 mac idea配置了八百次maven都不成功,mac source不生效 source ~/.bash_profile Alisa_wu666 ideamavensourcemaven 终端输入open~/.bash_profile,检查内容:exportPATH=/usr/bin:/usr/sbin:/bin:/sbinexportJAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/HomeexportM2_HOME=/Users/nanwu/mooc/apache-maven-3.5.3e 后端java的复习-常用API(个人笔记) 狴犴ys java基础后端技术栈巩固复习java 常用apiObjecttoString方法equals方法finalize方法clone()方法SystemString构造方法常用方法获取型判断型转换型StringBuffer与StringBuilder八种基本数据类型包装类八种包装类IntegerDate获取系统当前时间Date->StringString->DateCalendar(日历)介绍常用方法匿名内部类ObjecttoString方 秒杀业务中的库存扣减为什么不加分布式锁? 提前退休了-程序员阿飞 分布式 前言说到秒杀业务的库存扣减,就还是得先确认我们的扣减基本方案。秒杀场景的库存扣减方案一般的做法是,先在Redis中做扣减,然后发送一个MQ消息,消费者在接到消息之后做数据库中库存的真正扣减及业务逻辑操作。如何解决数据一致性问题:Redis中库存成功扣减了,但是后续发送MQ消息失败,或者后面的消费过程中消息丢了或者失败了等情况。就会导致Redis中的库存被扣减了,但是数据库库存没扣减,业务的实际操作 Kotlin/Java 实现 Html 转图片 zimoyin javakotlinhtml 1.Html2Imagegui.avahtml2image2.0.1StringHtmlTemplateStr="....";//HTMLHtmlParserhtmlParser=newHtmlParserImpl();htmlParser.loadHtml(HtmlTemplateStr);ImageRendererimageRenderer=newImageRendererImpl(htmlP 【赵渝强老师】监控Redis 数据库nosqlredis 对运行状态的Redis实例进行监控是运维管理中非常重要的内容,包括:监控Redis的内存、监控Redis的吞吐量、监控Redis的运行时信息和监控Redis的延时。通过Redis提供的监控命令便能非常方便地实现对各项指标的监控。一、监控Redis的内存视频讲解如下:https://www.bilibili.com/video/BV1mi28YXE14/?aid=113294358616...Red jQuery获取并解析API接口Json数据的方法 qhdzj87 前端(JSjQuery等)jqueryjsonjavascript json是API的常用数据交换格式,在Web开发中,除了后端程序,前端也可以借助JavaScript技术获取并解析API中的json数据,这里介绍一种通过jQuery的$get()函数获取并解析API接口Json数据的方法。其基本语法如下:$get(url,[data],[callback])在$get()函数中,包含了三个参数。其中:url:请求的地址;data:请求数据的列表;callback 如何设置HTTP请求中的参数? 数据小爬虫@ http网络协议网络 在Java中设置HTTP请求的参数是爬虫开发中的一个常见任务。这可以通过多种方式实现,具体取决于你使用的库。以下将展示如何使用ApacheHttpClient和Java11+的HttpClient设置HTTP请求中的参数。一、使用ApacheHttpClient设置请求参数(一)添加依赖如果你使用的是Maven,可以在pom.xml文件中添加以下依赖:org.apache.httpcomponen 如何使用Java爬虫处理API接口返回的JSON数据? 小爬虫程序猿 APIjavajson开发语言 处理API接口返回的JSON数据是Java爬虫开发中的一个常见任务。在Java中,有多个库可以帮助我们解析JSON数据,其中最流行的是Jackson和Gson。以下是使用这两个库处理JSON数据的基本步骤和示例代码。使用Jackson处理JSONJackson是一个功能强大的JSON处理库,它不仅可以将JSON字符串解析为Java对象,还可以将Java对象转换为JSON字符串。添加Jackson依 Spring Cache缓存注解深度解析 coder lei spring缓存java SpringCache缓存注解深度解析一、框架概述SpringCache是Spring框架提供的抽象缓存层,通过注解实现声明式缓存,与具体缓存实现(如Redis、Ehcache)解耦。核心接口CacheManager负责管理不同缓存,@EnableCaching开启注解驱动。二、核心注解详解1.@Cacheable作用:方法结果缓存,首次调用后缓存结果关键参数:@Cacheable(value=" 网络原理 初识[Java EE] 猿周LV JavaEE网络原理网络java-eejava 目录网络发展史独立模式网络互联局域网LAN1.基于网络直连2.基于集线器(Hub)组建3.基于交换机(Switch)组建4.基于交换机和路由器(Router)组建广域网WAN网络通信基础IP地址1.概念2.格式端口号1.概念2.格式认识协议1.概念2.作用3.协议分层3.1什么是协议分层3.2分层的作用3.2.1上层不需要了解下层的细节(封装)3.2.2灵活的调整/替换某层的协议4.五元组4.1源 Redis--单线程模型 04Koi. Redisredis数据库缓存 目录一、引言二、Redis单线程模型三、原因四、为什么redis是单线程模型,但他的速度这么快?五、总结一、引言本篇文章就Redis为什么是单线程模型做简单介绍。二、Redis单线程模型redis只使用一个线程,处理所有的命令请求,但是不是说redis服务器内部真的就只有一个线程,其实也有多个线程,这些线程在处理网络IO。假设同时有两个客户端向redis服务器发送了命令请求,但是redis还是会将 C++ vector::push_back和Java List.add的区别 da_kao_la JavaCppJavaCppvectorList C++vector::push_back和JavaList.add的区别对象赋值C++和Java在对象赋值方面区别较大,归根到底,还是因为C++和Java中对象存储和引用方式不同。C++中静态建立(即Objobj)的对象的内容(类属性)是存储在栈上的,变量obj直接持有对象本身;Java中建立的对象(Objobj=newObj())的内容(类属性)是存储在堆上的,位于栈上的变量obj只是持有对象的 集合框架 天子之骄 java数据结构集合框架 集合框架 集合框架可以理解为一个容器,该容器主要指映射(map)、集合(set)、数组(array)和列表(list)等抽象数据结构。 从本质上来说,Java集合框架的主要组成是用来操作对象的接口。不同接口描述不同的数据类型。 简单介绍: Collection接口是最基本的接口,它定义了List和Set,List又定义了LinkLi Table Driven(表驱动)方法实例 bijian1013 javaenumTable Driven表驱动 实例一: /** * 驾驶人年龄段 * 保险行业,会对驾驶人的年龄做年龄段的区分判断 * 驾驶人年龄段:01-[18,25);02-[25,30);03-[30-35);04-[35,40);05-[40,45);06-[45,50);07-[50-55);08-[55,+∞) */ public class AgePeriodTest { //if...el Jquery 总结 cuishikuan javajqueryAjaxWebjquery方法 1.$.trim方法用于移除字符串头部和尾部多余的空格。如:$.trim(' Hello ') // Hello2.$.contains方法返回一个布尔值,表示某个DOM元素(第二个参数)是否为另一个DOM元素(第一个参数)的下级元素。如:$.contains(document.documentElement, document.body); 3.$ 面向对象概念的提出 麦田的设计者 java面向对象面向过程 面向对象中,一切都是由对象展开的,组织代码,封装数据。 在台湾面向对象被翻译为了面向物件编程,这充分说明了,这种编程强调实体。 下面就结合编程语言的发展史,聊一聊面向过程和面向对象。 c语言由贝尔实 linux网口绑定 被触发 linux 刚在一台IBM Xserver服务器上装了RedHat Linux Enterprise AS 4,为了提高网络的可靠性配置双网卡绑定。 一、环境描述 我的RedHat Linux Enterprise AS 4安装双口的Intel千兆网卡,通过ifconfig -a命令看到eth0和eth1两张网卡。 二、双网卡绑定步骤: 2.1 修改/etc/sysconfig/network XML基础语法 肆无忌惮_ xml 一、什么是XML? XML全称是Extensible Markup Language,可扩展标记语言。很类似HTML。XML的目的是传输数据而非显示数据。XML的标签没有被预定义,你需要自行定义标签。XML被设计为具有自我描述性。是W3C的推荐标准。 二、为什么学习XML? 用来解决程序间数据传输的格式问题 做配置文件 充当小型数据库 三、XML与HTM 为网页添加自己喜欢的字体 知了ing 字体 秒表 css @font-face { font-family: miaobiao;//定义字体名字 font-style: normal; font-weight: 400; src: url('font/DS-DIGI-e.eot');//字体文件 } 使用: <label style="font-size:18px;font-famil redis范围查询应用-查找IP所在城市 矮蛋蛋 redis 原文地址: http://www.tuicool.com/articles/BrURbqV 需求 根据IP找到对应的城市 原来的解决方案 oracle表(ip_country): 查询IP对应的城市: 1.把a.b.c.d这样格式的IP转为一个数字,例如为把210.21.224.34转为3524648994 2. select city from ip_ 输入两个整数, 计算百分比 alleni123 java public static String getPercent(int x, int total){ double result=(x*1.0)/(total*1.0); System.out.println(result); DecimalFormat df1=new DecimalFormat("0.0000%"); 百合——————>怎么学习计算机语言 百合不是茶 java 移动开发 对于一个从没有接触过计算机语言的人来说,一上来就学面向对象,就算是心里上面接受的了,灵魂我觉得也应该是跟不上的,学不好是很正常的现象,计算机语言老师讲的再多,你在课堂上面跟着老师听的再多,我觉得你应该还是学不会的,最主要的原因是你根本没有想过该怎么来学习计算机编程语言,记得大一的时候金山网络公司在湖大招聘我们学校一个才来大学几天的被金山网络录取,一个刚到大学的就能够去和 linux下tomcat开机自启动 bijian1013 tomcat 方法一: 修改Tomcat/bin/startup.sh 为: export JAVA_HOME=/home/java1.6.0_27 export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:. export PATH=$JAVA_HOME/bin:$PATH export CATALINA_H spring aop实例 bijian1013 javaspringAOP 1.AdviceMethods.java package com.bijian.study.spring.aop.schema; public class AdviceMethods { public void preGreeting() { System.out.println("--how are you!--"); } } 2.beans.x [Gson八]GsonBuilder序列化和反序列化选项enableComplexMapKeySerialization bit1129 serialization enableComplexMapKeySerialization配置项的含义 Gson在序列化Map时,默认情况下,是调用Key的toString方法得到它的JSON字符串的Key,对于简单类型和字符串类型,这没有问题,但是对于复杂数据对象,如果对象没有覆写toString方法,那么默认的toString方法将得到这个对象的Hash地址。 GsonBuilder用于 【Spark九十一】Spark Streaming整合Kafka一些值得关注的问题 bit1129 Stream 包括Spark Streaming在内的实时计算数据可靠性指的是三种级别: 1. At most once,数据最多只能接受一次,有可能接收不到 2. At least once, 数据至少接受一次,有可能重复接收 3. Exactly once 数据保证被处理并且只被处理一次, 具体的多读几遍http://spark.apache.org/docs/lates shell脚本批量检测端口是否被占用脚本 ronin47 #!/bin/bash cat ports |while read line do#nc -z -w 10 $line nc -z -w 2 $line 58422>/dev/null2>&1if[ $?-eq 0]then echo $line:ok else echo $line:fail fi done 这里的ports 既可以是文件 java-2.设计包含min函数的栈 bylijinnan java 具体思路参见:http://zhedahht.blog.163.com/blog/static/25411174200712895228171/ import java.util.ArrayList; import java.util.List; public class MinStack { //maybe we can use origin array rathe Netty源码学习-ChannelHandler bylijinnan javanetty 一般来说,“有状态”的ChannelHandler不应该是“共享”的,“无状态”的ChannelHandler则可“共享” 例如ObjectEncoder是“共享”的, 但 ObjectDecoder 不是 因为每一次调用decode方法时,可能数据未接收完全(incomplete), 它与上一次decode时接收到的数据“累计”起来才有可能是完整的数据,是“有状态”的 p java生成随机数 cngolon java 方法一: /** * 生成随机数 * @author cngolon@126.com * @return */ public synchronized static String getChargeSequenceNum(String pre){ StringBuffer sequenceNum = new StringBuffer(); Date dateTime = new D POI读写海量数据 ctrain 海量数据 import java.io.FileOutputStream; import java.io.OutputStream; import org.apache.poi.xssf.streaming.SXSSFRow; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming mysql 日期格式化date_format详细使用 daizj mysqldate_format日期格式转换日期格式化 日期转换函数的详细使用说明 DATE_FORMAT(date,format) Formats the date value according to the format string. The following specifiers may be used in the format string. The&n 一个程序员分享8年的开发经验 dcj3sjt126com 程序员 在中国有很多人都认为IT行为是吃青春饭的,如果过了30岁就很难有机会再发展下去!其实现实并不是这样子的,在下从事.NET及JAVA方面的开发的也有8年的时间了,在这里在下想凭借自己的亲身经历,与大家一起探讨一下。 明确入行的目的 很多人干IT这一行都冲着“收入高”这一点的,因为只要学会一点HTML, DIV+CSS,要做一个页面开发人员并不是一件难事,而且做一个页面开发人员更容 android欢迎界面淡入淡出效果 dcj3sjt126com android 很多Android应用一开始都会有一个欢迎界面,淡入淡出效果也是用得非常多的,下面来实现一下。 主要代码如下: package com.myaibang.activity; import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.os.CountDown linux 复习笔记之常见压缩命令 eksliang tar解压linux系统常见压缩命令linux压缩命令tar压缩 转载请出自出处:http://eksliang.iteye.com/blog/2109693 linux中常见压缩文件的拓展名 *.gz gzip程序压缩的文件 *.bz2 bzip程序压缩的文件 *.tar tar程序打包的数据,没有经过压缩 *.tar.gz tar程序打包后,并经过gzip程序压缩 *.tar.bz2 tar程序打包后,并经过bzip程序压缩 *.zi Android 应用程序发送shell命令 gqdy365 android 项目中需要直接在APP中通过发送shell指令来控制lcd灯,其实按理说应该是方案公司在调好lcd灯驱动之后直接通过service送接口上来给APP,APP调用就可以控制了,这是正规流程,但我们项目的方案商用的mtk方案,方案公司又没人会改,只调好了驱动,让应用程序自己实现灯的控制,这不蛋疼嘛!!!! 发就发吧! 一、关于shell指令: 我们知道,shell指令是Linux里面带的 java 无损读取文本文件 hw1287789687 读取文件无损读取读取文本文件charset java 如何无损读取文本文件呢? 以下是有损的 @Deprecated public static String getFullContent(File file, String charset) { BufferedReader reader = null; if (!file.exists()) { System.out.println("getFull Firebase 相关文章索引 justjavac firebase Awesome Firebase 最近谷歌收购Firebase的新闻又将Firebase拉入了人们的视野,于是我做了这个 github 项目。 Firebase 是一个数据同步的云服务,不同于 Dropbox 的「文件」,Firebase 同步的是「数据」,服务对象是网站开发者,帮助他们开发具有「实时」(Real-Time)特性的应用。 开发者只需引用一个 API 库文件就可以使用标准 RE C++学习重点 lx.asymmetric C++笔记 1.c++面向对象的三个特性:封装性,继承性以及多态性。 2.标识符的命名规则:由字母和下划线开头,同时由字母、数字或下划线组成;不能与系统关键字重名。 3.c++语言常量包括整型常量、浮点型常量、布尔常量、字符型常量和字符串性常量。 4.运算符按其功能开以分为六类:算术运算符、位运算符、关系运算符、逻辑运算符、赋值运算符和条件运算符。 &n java bean和xml相互转换 q821424508 javabeanxmlxml和bean转换java bean和xml转换 这几天在做微信公众号 做的过程中想找个java bean转xml的工具,找了几个用着不知道是配置不好还是怎么回事,都会有一些问题, 然后脑子一热谢了一个javabean和xml的转换的工具里,自己用着还行,虽然有一些约束吧 , 还是贴出来记录一下 顺便你提一下下,这个转换工具支持属性为集合、数组和非基本属性的对象。 packag C 语言初级 位运算 1140566087 位运算c 第十章 位运算 1、位运算对象只能是整形或字符型数据,在VC6.0中int型数据占4个字节 2、位运算符: 运算符 作用 ~ 按位求反 << 左移 >> 右移 & 按位与 ^ 按位异或 | 按位或 他们的优先级从高到低; 3、位运算符的运算功能: a、按位取反: ~01001101 = 101 14点睛Spring4.1-脚本编程 wiselyman spring4 14.1 Scripting脚本编程 脚本语言和java这类静态的语言的主要区别是:脚本语言无需编译,源码直接可运行; 如果我们经常需要修改的某些代码,每一次我们至少要进行编译,打包,重新部署的操作,步骤相当麻烦; 如果我们的应用不允许重启,这在现实的情况中也是很常见的; 在spring中使用脚本编程给上述的应用场景提供了解决方案,即动态加载bean; spring支持脚本 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。
严谨的小张觉得虽然当个普通互斥锁,已经稳稳够用,可是业务里总是又很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题。
而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。
小张不是杠精,因为库存操作总有这样那样的特殊。
所以我们希望在这种情况时,可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果,这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约。
读写分离也是常见,一个读多写少的业务为了性能,常常是有读锁和写锁的。
而此刻的扩展已经超出了一把简单轮子的复杂程度,光是处理续约,就够小张喝一壶,何况在性能(锁的最大等待时间)、优雅(无效锁申请)、重试(失败重试机制)等方面还要下功夫研究。在小张苦思冥想时,旁边的小白凑过来看了看小张,很好奇,都2021年了,为什么不直接用redisson呢?
Redisson就有这把你要的锁。
号称简单的Redisson分布式锁的使用姿势是什么?
1.依赖
org.redisson redisson 3.13.6 org.redisson redisson-spring-boot-starter 3.13.6 复制代码
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.启用分布式锁
@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剩余时间
判断该锁是否已经有对应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())
Arrays.asList(getName(), getChannelName())
name 锁名称 channelName,用于pubSub发布消息的channel名称 复制代码
ARGV变量有三个LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)
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;
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模型(参考Netty中的异步编程),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
• 添加一个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方法。
NonfairSync
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。
保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则
为什么JUC以默认非公平锁呢?
因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。
可见公平的代价是性能与吞吐量。
Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。
RedissonFairLock 用法依然很简单
RLock fairLock = redissonClient.getFairLock(lockName); fairLock.lock();
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; 复制代码 1.公平锁加锁步骤 通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。 其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为6个步骤 队列清理 保证队列中只有未过期的等待线程 首次加锁 hset加锁,pexpire过期时间 重入判断 此处同可重入锁lua 返回ttl 计算尾节点ttl 初始值为锁的剩余过期时间 末尾排队 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通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 六、总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现,Redisson也提供了多机情况下的联锁(MultiLock)和官方推荐的红锁(RedLock),下一章再详细介绍。 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(redis,java,redis,开发语言) SpringBoot整合DeepSeek技术指南(2025版) hjy1821 AI人工智能 SpringBoot整合DeepSeek技术指南(2025版)环境准备com.deepseekdeepseek-java-sdk2.5.0org.springframework.bootspring-boot-starter-webflux配置中心设置#application.ymldeepseek:api:base-url:https://api.deepseek.com/v2token:${D Oracle 数据库基础入门(五):限制查询与范式三约定深度解析 Aphelios380 Oracle数据库oracle学习java 在Oracle数据库的学习进程中,限制查询与范式三约定是两个极为重要的概念。限制查询帮助我们精准获取特定范围的数据,而范式三约定则为数据库设计提供了科学的指导框架。对于Java全栈开发者而言,掌握这些知识不仅有助于高效地从数据库中提取数据,更能设计出结构合理、性能优良的数据库,为构建强大的应用系统奠定坚实基础。目录一、Oracle限制查询(一)与MySQL限制查询的对比(二)伪列的奥秘(三)限制查 7-11 sdut-String-5 图书价格汇总(II) (10 分) 灯火穿透了 PTAJava题java 假设图书馆中图书信息的格式为:Java程序设计:34;Web程序设计:56;JSP程序设计:20按要求输出每本图书的名称及价格,计算所有图书的总价格并输出。输入格式:读入一行图书信息。如:Java程序设计:34;Web程序设计:56;JSP程序设计:20提示:每本书的价格是整数,价格与下一本书的名字之间有一个中文;价格前可能有空格,可能没有。输出格式:分别输出每本图书的名称及价格,一本书占一行,形 三、Jvm内存分配 刘总Java Java虚拟机jvmjava开发语言 今天的博客主题Java虚拟机——》Jvm内存分配什么是JVM内存分配呢?就是当我们创建一个对象的时候,要在JVM内存空间里为这个对象分配一些空间,来存放对象的一些属性信息。对象创建的流程1)类加载检查在创建对象的过程中,也就是new一个对象的时候。首先检查new指令的参数在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如有没有,说明这个类没有被加载使用过 Java生成LRC纵向冗余校验 YunFeiDong Javajava开发语言ModbusASCII 纵向冗余校验(LongitudinalRedundancyCheck,简称:LRC)是通信中常用的一种校验形式,也称LRC校验或纵向校验;它是一种从纵向通道上的特定比特串产生校验比特的错误检测方法;通常Modbus协议ASCII模式采用LRC算法。1.生成LRC校验/***生成LRC校验值:**1)对需要校验的数据(2n个字符)两两组成一个16进制的数值求和;*2)将求和结果与256求模;*3)用 Java基础之JVM对象内存分配机制简介 aoneword JVM基础jvmjava开发语言 一对象内存分配1.1运行时数据区域1.2常见java应用启动JVM参数:-Xss:每个线程的栈大小(单位kb)-Xms:堆的初始大小,默认物理内存的1/64,示例:-Xms:4g-Xms:10m-Xmx:堆的最大可用大小,默认物理内存的1/4-Xmn:新生代大小-XX:NewRatio:默认2,表示新生代占老年代的1/2,占整个堆内存的1/3-XX:SurvivorRatio:默认8,表示一个su python文件修改后不生效_pycharm修改代码后第一次运行不生效解决 weixin_39959126 python文件修改后不生效 SVN搭建简单教程一.引言笔者曾经试图在网上搜索一篇关于SVN源代码服务器搭建方面的中文技术文章,可惜,所找到的,要么是不完整,要么就是对笔者没什么帮助的文章,TortoiseSvn的帮助文档固然强大,但因为是英文,...java零碎知识点1.字符串有整型的相互转换12Stringa=String.valueOf(2);//integertonumericstringinti=Integer.pa react 基础 理想和远方_在路上 react.jsjsx 一.什么是react?react是一种用于构建用户界面的javaScript库react主要用来写html,或构建web应用如果用mvc的角度来看,react仅仅是视图层(v),也就是只负责视图的渲染,并不是提供了完成的M和C的功能。二.react的特点1.声明式只需要描述html看起来是什么样,就跟写html一样。react负责渲染UI,并在数据变化是更新UI。constjsx=HelloRea java excel 导入 加校验_Java通过POI为Excel添加数据验证 夏至未至 javaexcel导入加校验 Stringpath="d:\\success.xlsx";StringsheetName="sheetlist";XSSFWorkbookwb=null;XSSFSheetsheetlist=null;FileinputFile=newFile(path);if(inputFile.exists()){wb=newXSSFWorkbook(newFileInputStream(path));}e SpringAI赋能Java开发打造智能应用 java技术小馆 javaAI编程 一、SpringAI是什么?为什么你需要它?想象一下,你的Java应用能够:理解自然语言自动生成代码智能分析数据提供个性化推荐这就是SpringAI带来的变革!它是Spring官方推出的AI集成框架,让你的Java应用轻松获得AI能力。传统开发vsSpringAI开发对比:能力传统开发SpringAI开发自然语言处理需要集成第三方SDK开箱即用开发效率手动实现复杂逻辑自动生成代码维护成本高低可扩展 IDEA编写JAVA的常用快捷键 峥嵘轻稠 intellij-ideajavaintellijidea (摘要:这是我平常使用IDEA的快捷键,希望能帮到和我一样刚入门的小白~~创作不易,希望能得到点赞关注支持嘿嘿嘿~~~)Ctrl+A:全选Ctrl+Z:撤销Ctrl+X:剪切Ctrl+C:复制Ctrl+V:粘贴Ctrl+Y:删除当前行Ctrl+D:复制当前行到下一行Ctrl+O:选择可重写的方法Ctrl+I:重写接口方法Ctrl+/:快速单行注释Ctrl+Shift+/:快速多行注释Ctrl+Al JAVA学习——DAY1 E卤蛋 JAVA学习java 几个cmd终端命令:cls——清屏cd——移至所在路径,后跟绝对路径or相对路径ip-config——查看本机ip信息java开发环境:JDK——java开发环境,包含:JRE(Java运行时环境),JVM(java虚拟机),以及各类开发库文件;安装目录:bin目录:两个重要——javac.exe(java编译工具),从.java文件编译为.class(字节码文件),java.exe(Java运行 JVM两种内存分配方式 小白,想脱白 jvm 1、指针碰撞如果jvm内存是规整的,就是一边是已使用的内存,另外一边是未使用的内存,中间是指针,jvm需要给新对象分配内存时,就会将指针往未使用的内存移动一段和新对象一样大小的距离,这样jvm就完成了内存分配。2、空闲列表如果jvm内存不是规整的,已使用的,未使用的内存,都是杂乱无章的,这个时候就不可能使用指针碰撞,Java虚拟机就会维护一个空闲列表,用来记录哪些内存是可用的,然后jvm给新对象分 JAVA多线程详解(超详细) m0_74823434 面试学习路线阿里巴巴资料职业发展javapython开发语言后端 目录一、线程简介1、进程、线程2、并发、并行、串行3、进程的三态二、线程实现1、继承Thread类2、实现Runnable接口3、实现Callable接口(不常用)三、线程常用方法1、线程的状态2、线程常用方法四、多线程1、守护(Deamon)线程2、多线程并发与同步3、死锁4、Lock(锁)5、线程协作6、线程池一、线程简介1、进程、线程程序:开发写的代码称之为程序。程序就是一堆代码,一组数据和 JMeter 不同协议测试最佳实践汇总 那片海还在吗 testjmeter测试工具 JMeter不同协议测试最佳实践汇总一、JMeter测试HTTPS(一)环境准备JMeter安装:从JMeter官方网站下载并解压JMeter到本地。Java环境:确保系统已安装Java运行环境(JRE)或开发环境(JDK),建议使用Java8及以上版本。(二)创建测试计划启动JMeter,默认有一个测试计划。右键点击测试计划,选择“添加”->“线程(用户)”->“线程组”,可设置线程数、循环次数 【React全解】React起手式 caihuayuan4 面试题汇总与解析springsqljava大数据 如何引入React从bootcdn引入React按顺序引入React然后引入ReactDOMCommonJSVSUMD规范JavaScript的模块定义和加载机制,降低了学习和使用各种框架的门槛,能够以一种统一的方式去定义和使用模块,提高开发效率,降低了应用维护成本CommonJSCommonJS是一种规范,NodeJS是这种规范的实现CommonJS模块是对象,是运行时加载,运行时才把模块挂载在 com.mysql.jdbc.Driver 和 com.mysql.cj.jdbc.Driver的区别 caihuayuan4 面试题汇总与解析springsqljava大数据课程设计 com.mysql.jdbc.Driver是mysql-connector-java5中的com.mysql.cj.jdbc.Driver是mysql-connector-java6中的所以在使用的时候一定要注意版本com.mysql.jdbc.DriverdriverClassName=com.mysql.jdbc.Driverurl=jdbc:mysql://localhost:3306/te Java学习——day14 blackA_ java学习开发语言 文章目录1.项目需求分析2.项目设计3.代码分析4.运行示例5.今日学习总结6.今日生词今日学习计划1.项目需求分析功能要求:(1)存储学生信息:使用HashMap存储学生信息(学号作为键,Student对象作为值)。(2)操作学生数据:添加学生(姓名、学号、成绩)。删除学生(按学号删除)。查询学生(按学号查询)。显示所有学生信息。(3)异常处理:防止重复添加(如果学号已存在,抛出异常)。查询/删 mac idea配置了八百次maven都不成功,mac source不生效 source ~/.bash_profile Alisa_wu666 ideamavensourcemaven 终端输入open~/.bash_profile,检查内容:exportPATH=/usr/bin:/usr/sbin:/bin:/sbinexportJAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/HomeexportM2_HOME=/Users/nanwu/mooc/apache-maven-3.5.3e 后端java的复习-常用API(个人笔记) 狴犴ys java基础后端技术栈巩固复习java 常用apiObjecttoString方法equals方法finalize方法clone()方法SystemString构造方法常用方法获取型判断型转换型StringBuffer与StringBuilder八种基本数据类型包装类八种包装类IntegerDate获取系统当前时间Date->StringString->DateCalendar(日历)介绍常用方法匿名内部类ObjecttoString方 秒杀业务中的库存扣减为什么不加分布式锁? 提前退休了-程序员阿飞 分布式 前言说到秒杀业务的库存扣减,就还是得先确认我们的扣减基本方案。秒杀场景的库存扣减方案一般的做法是,先在Redis中做扣减,然后发送一个MQ消息,消费者在接到消息之后做数据库中库存的真正扣减及业务逻辑操作。如何解决数据一致性问题:Redis中库存成功扣减了,但是后续发送MQ消息失败,或者后面的消费过程中消息丢了或者失败了等情况。就会导致Redis中的库存被扣减了,但是数据库库存没扣减,业务的实际操作 Kotlin/Java 实现 Html 转图片 zimoyin javakotlinhtml 1.Html2Imagegui.avahtml2image2.0.1StringHtmlTemplateStr="....";//HTMLHtmlParserhtmlParser=newHtmlParserImpl();htmlParser.loadHtml(HtmlTemplateStr);ImageRendererimageRenderer=newImageRendererImpl(htmlP 【赵渝强老师】监控Redis 数据库nosqlredis 对运行状态的Redis实例进行监控是运维管理中非常重要的内容,包括:监控Redis的内存、监控Redis的吞吐量、监控Redis的运行时信息和监控Redis的延时。通过Redis提供的监控命令便能非常方便地实现对各项指标的监控。一、监控Redis的内存视频讲解如下:https://www.bilibili.com/video/BV1mi28YXE14/?aid=113294358616...Red jQuery获取并解析API接口Json数据的方法 qhdzj87 前端(JSjQuery等)jqueryjsonjavascript json是API的常用数据交换格式,在Web开发中,除了后端程序,前端也可以借助JavaScript技术获取并解析API中的json数据,这里介绍一种通过jQuery的$get()函数获取并解析API接口Json数据的方法。其基本语法如下:$get(url,[data],[callback])在$get()函数中,包含了三个参数。其中:url:请求的地址;data:请求数据的列表;callback 如何设置HTTP请求中的参数? 数据小爬虫@ http网络协议网络 在Java中设置HTTP请求的参数是爬虫开发中的一个常见任务。这可以通过多种方式实现,具体取决于你使用的库。以下将展示如何使用ApacheHttpClient和Java11+的HttpClient设置HTTP请求中的参数。一、使用ApacheHttpClient设置请求参数(一)添加依赖如果你使用的是Maven,可以在pom.xml文件中添加以下依赖:org.apache.httpcomponen 如何使用Java爬虫处理API接口返回的JSON数据? 小爬虫程序猿 APIjavajson开发语言 处理API接口返回的JSON数据是Java爬虫开发中的一个常见任务。在Java中,有多个库可以帮助我们解析JSON数据,其中最流行的是Jackson和Gson。以下是使用这两个库处理JSON数据的基本步骤和示例代码。使用Jackson处理JSONJackson是一个功能强大的JSON处理库,它不仅可以将JSON字符串解析为Java对象,还可以将Java对象转换为JSON字符串。添加Jackson依 Spring Cache缓存注解深度解析 coder lei spring缓存java SpringCache缓存注解深度解析一、框架概述SpringCache是Spring框架提供的抽象缓存层,通过注解实现声明式缓存,与具体缓存实现(如Redis、Ehcache)解耦。核心接口CacheManager负责管理不同缓存,@EnableCaching开启注解驱动。二、核心注解详解1.@Cacheable作用:方法结果缓存,首次调用后缓存结果关键参数:@Cacheable(value=" 网络原理 初识[Java EE] 猿周LV JavaEE网络原理网络java-eejava 目录网络发展史独立模式网络互联局域网LAN1.基于网络直连2.基于集线器(Hub)组建3.基于交换机(Switch)组建4.基于交换机和路由器(Router)组建广域网WAN网络通信基础IP地址1.概念2.格式端口号1.概念2.格式认识协议1.概念2.作用3.协议分层3.1什么是协议分层3.2分层的作用3.2.1上层不需要了解下层的细节(封装)3.2.2灵活的调整/替换某层的协议4.五元组4.1源 Redis--单线程模型 04Koi. Redisredis数据库缓存 目录一、引言二、Redis单线程模型三、原因四、为什么redis是单线程模型,但他的速度这么快?五、总结一、引言本篇文章就Redis为什么是单线程模型做简单介绍。二、Redis单线程模型redis只使用一个线程,处理所有的命令请求,但是不是说redis服务器内部真的就只有一个线程,其实也有多个线程,这些线程在处理网络IO。假设同时有两个客户端向redis服务器发送了命令请求,但是redis还是会将 C++ vector::push_back和Java List.add的区别 da_kao_la JavaCppJavaCppvectorList C++vector::push_back和JavaList.add的区别对象赋值C++和Java在对象赋值方面区别较大,归根到底,还是因为C++和Java中对象存储和引用方式不同。C++中静态建立(即Objobj)的对象的内容(类属性)是存储在栈上的,变量obj直接持有对象本身;Java中建立的对象(Objobj=newObj())的内容(类属性)是存储在堆上的,位于栈上的变量obj只是持有对象的 集合框架 天子之骄 java数据结构集合框架 集合框架 集合框架可以理解为一个容器,该容器主要指映射(map)、集合(set)、数组(array)和列表(list)等抽象数据结构。 从本质上来说,Java集合框架的主要组成是用来操作对象的接口。不同接口描述不同的数据类型。 简单介绍: Collection接口是最基本的接口,它定义了List和Set,List又定义了LinkLi Table Driven(表驱动)方法实例 bijian1013 javaenumTable Driven表驱动 实例一: /** * 驾驶人年龄段 * 保险行业,会对驾驶人的年龄做年龄段的区分判断 * 驾驶人年龄段:01-[18,25);02-[25,30);03-[30-35);04-[35,40);05-[40,45);06-[45,50);07-[50-55);08-[55,+∞) */ public class AgePeriodTest { //if...el Jquery 总结 cuishikuan javajqueryAjaxWebjquery方法 1.$.trim方法用于移除字符串头部和尾部多余的空格。如:$.trim(' Hello ') // Hello2.$.contains方法返回一个布尔值,表示某个DOM元素(第二个参数)是否为另一个DOM元素(第一个参数)的下级元素。如:$.contains(document.documentElement, document.body); 3.$ 面向对象概念的提出 麦田的设计者 java面向对象面向过程 面向对象中,一切都是由对象展开的,组织代码,封装数据。 在台湾面向对象被翻译为了面向物件编程,这充分说明了,这种编程强调实体。 下面就结合编程语言的发展史,聊一聊面向过程和面向对象。 c语言由贝尔实 linux网口绑定 被触发 linux 刚在一台IBM Xserver服务器上装了RedHat Linux Enterprise AS 4,为了提高网络的可靠性配置双网卡绑定。 一、环境描述 我的RedHat Linux Enterprise AS 4安装双口的Intel千兆网卡,通过ifconfig -a命令看到eth0和eth1两张网卡。 二、双网卡绑定步骤: 2.1 修改/etc/sysconfig/network XML基础语法 肆无忌惮_ xml 一、什么是XML? XML全称是Extensible Markup Language,可扩展标记语言。很类似HTML。XML的目的是传输数据而非显示数据。XML的标签没有被预定义,你需要自行定义标签。XML被设计为具有自我描述性。是W3C的推荐标准。 二、为什么学习XML? 用来解决程序间数据传输的格式问题 做配置文件 充当小型数据库 三、XML与HTM 为网页添加自己喜欢的字体 知了ing 字体 秒表 css @font-face { font-family: miaobiao;//定义字体名字 font-style: normal; font-weight: 400; src: url('font/DS-DIGI-e.eot');//字体文件 } 使用: <label style="font-size:18px;font-famil redis范围查询应用-查找IP所在城市 矮蛋蛋 redis 原文地址: http://www.tuicool.com/articles/BrURbqV 需求 根据IP找到对应的城市 原来的解决方案 oracle表(ip_country): 查询IP对应的城市: 1.把a.b.c.d这样格式的IP转为一个数字,例如为把210.21.224.34转为3524648994 2. select city from ip_ 输入两个整数, 计算百分比 alleni123 java public static String getPercent(int x, int total){ double result=(x*1.0)/(total*1.0); System.out.println(result); DecimalFormat df1=new DecimalFormat("0.0000%"); 百合——————>怎么学习计算机语言 百合不是茶 java 移动开发 对于一个从没有接触过计算机语言的人来说,一上来就学面向对象,就算是心里上面接受的了,灵魂我觉得也应该是跟不上的,学不好是很正常的现象,计算机语言老师讲的再多,你在课堂上面跟着老师听的再多,我觉得你应该还是学不会的,最主要的原因是你根本没有想过该怎么来学习计算机编程语言,记得大一的时候金山网络公司在湖大招聘我们学校一个才来大学几天的被金山网络录取,一个刚到大学的就能够去和 linux下tomcat开机自启动 bijian1013 tomcat 方法一: 修改Tomcat/bin/startup.sh 为: export JAVA_HOME=/home/java1.6.0_27 export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:. export PATH=$JAVA_HOME/bin:$PATH export CATALINA_H spring aop实例 bijian1013 javaspringAOP 1.AdviceMethods.java package com.bijian.study.spring.aop.schema; public class AdviceMethods { public void preGreeting() { System.out.println("--how are you!--"); } } 2.beans.x [Gson八]GsonBuilder序列化和反序列化选项enableComplexMapKeySerialization bit1129 serialization enableComplexMapKeySerialization配置项的含义 Gson在序列化Map时,默认情况下,是调用Key的toString方法得到它的JSON字符串的Key,对于简单类型和字符串类型,这没有问题,但是对于复杂数据对象,如果对象没有覆写toString方法,那么默认的toString方法将得到这个对象的Hash地址。 GsonBuilder用于 【Spark九十一】Spark Streaming整合Kafka一些值得关注的问题 bit1129 Stream 包括Spark Streaming在内的实时计算数据可靠性指的是三种级别: 1. At most once,数据最多只能接受一次,有可能接收不到 2. At least once, 数据至少接受一次,有可能重复接收 3. Exactly once 数据保证被处理并且只被处理一次, 具体的多读几遍http://spark.apache.org/docs/lates shell脚本批量检测端口是否被占用脚本 ronin47 #!/bin/bash cat ports |while read line do#nc -z -w 10 $line nc -z -w 2 $line 58422>/dev/null2>&1if[ $?-eq 0]then echo $line:ok else echo $line:fail fi done 这里的ports 既可以是文件 java-2.设计包含min函数的栈 bylijinnan java 具体思路参见:http://zhedahht.blog.163.com/blog/static/25411174200712895228171/ import java.util.ArrayList; import java.util.List; public class MinStack { //maybe we can use origin array rathe Netty源码学习-ChannelHandler bylijinnan javanetty 一般来说,“有状态”的ChannelHandler不应该是“共享”的,“无状态”的ChannelHandler则可“共享” 例如ObjectEncoder是“共享”的, 但 ObjectDecoder 不是 因为每一次调用decode方法时,可能数据未接收完全(incomplete), 它与上一次decode时接收到的数据“累计”起来才有可能是完整的数据,是“有状态”的 p java生成随机数 cngolon java 方法一: /** * 生成随机数 * @author cngolon@126.com * @return */ public synchronized static String getChargeSequenceNum(String pre){ StringBuffer sequenceNum = new StringBuffer(); Date dateTime = new D POI读写海量数据 ctrain 海量数据 import java.io.FileOutputStream; import java.io.OutputStream; import org.apache.poi.xssf.streaming.SXSSFRow; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming mysql 日期格式化date_format详细使用 daizj mysqldate_format日期格式转换日期格式化 日期转换函数的详细使用说明 DATE_FORMAT(date,format) Formats the date value according to the format string. The following specifiers may be used in the format string. The&n 一个程序员分享8年的开发经验 dcj3sjt126com 程序员 在中国有很多人都认为IT行为是吃青春饭的,如果过了30岁就很难有机会再发展下去!其实现实并不是这样子的,在下从事.NET及JAVA方面的开发的也有8年的时间了,在这里在下想凭借自己的亲身经历,与大家一起探讨一下。 明确入行的目的 很多人干IT这一行都冲着“收入高”这一点的,因为只要学会一点HTML, DIV+CSS,要做一个页面开发人员并不是一件难事,而且做一个页面开发人员更容 android欢迎界面淡入淡出效果 dcj3sjt126com android 很多Android应用一开始都会有一个欢迎界面,淡入淡出效果也是用得非常多的,下面来实现一下。 主要代码如下: package com.myaibang.activity; import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.os.CountDown linux 复习笔记之常见压缩命令 eksliang tar解压linux系统常见压缩命令linux压缩命令tar压缩 转载请出自出处:http://eksliang.iteye.com/blog/2109693 linux中常见压缩文件的拓展名 *.gz gzip程序压缩的文件 *.bz2 bzip程序压缩的文件 *.tar tar程序打包的数据,没有经过压缩 *.tar.gz tar程序打包后,并经过gzip程序压缩 *.tar.bz2 tar程序打包后,并经过bzip程序压缩 *.zi Android 应用程序发送shell命令 gqdy365 android 项目中需要直接在APP中通过发送shell指令来控制lcd灯,其实按理说应该是方案公司在调好lcd灯驱动之后直接通过service送接口上来给APP,APP调用就可以控制了,这是正规流程,但我们项目的方案商用的mtk方案,方案公司又没人会改,只调好了驱动,让应用程序自己实现灯的控制,这不蛋疼嘛!!!! 发就发吧! 一、关于shell指令: 我们知道,shell指令是Linux里面带的 java 无损读取文本文件 hw1287789687 读取文件无损读取读取文本文件charset java 如何无损读取文本文件呢? 以下是有损的 @Deprecated public static String getFullContent(File file, String charset) { BufferedReader reader = null; if (!file.exists()) { System.out.println("getFull Firebase 相关文章索引 justjavac firebase Awesome Firebase 最近谷歌收购Firebase的新闻又将Firebase拉入了人们的视野,于是我做了这个 github 项目。 Firebase 是一个数据同步的云服务,不同于 Dropbox 的「文件」,Firebase 同步的是「数据」,服务对象是网站开发者,帮助他们开发具有「实时」(Real-Time)特性的应用。 开发者只需引用一个 API 库文件就可以使用标准 RE C++学习重点 lx.asymmetric C++笔记 1.c++面向对象的三个特性:封装性,继承性以及多态性。 2.标识符的命名规则:由字母和下划线开头,同时由字母、数字或下划线组成;不能与系统关键字重名。 3.c++语言常量包括整型常量、浮点型常量、布尔常量、字符型常量和字符串性常量。 4.运算符按其功能开以分为六类:算术运算符、位运算符、关系运算符、逻辑运算符、赋值运算符和条件运算符。 &n java bean和xml相互转换 q821424508 javabeanxmlxml和bean转换java bean和xml转换 这几天在做微信公众号 做的过程中想找个java bean转xml的工具,找了几个用着不知道是配置不好还是怎么回事,都会有一些问题, 然后脑子一热谢了一个javabean和xml的转换的工具里,自己用着还行,虽然有一些约束吧 , 还是贴出来记录一下 顺便你提一下下,这个转换工具支持属性为集合、数组和非基本属性的对象。 packag C 语言初级 位运算 1140566087 位运算c 第十章 位运算 1、位运算对象只能是整形或字符型数据,在VC6.0中int型数据占4个字节 2、位运算符: 运算符 作用 ~ 按位求反 << 左移 >> 右移 & 按位与 ^ 按位异或 | 按位或 他们的优先级从高到低; 3、位运算符的运算功能: a、按位取反: ~01001101 = 101 14点睛Spring4.1-脚本编程 wiselyman spring4 14.1 Scripting脚本编程 脚本语言和java这类静态的语言的主要区别是:脚本语言无需编译,源码直接可运行; 如果我们经常需要修改的某些代码,每一次我们至少要进行编译,打包,重新部署的操作,步骤相当麻烦; 如果我们的应用不允许重启,这在现实的情况中也是很常见的; 在spring中使用脚本编程给上述的应用场景提供了解决方案,即动态加载bean; spring支持脚本 按字母分类: 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; 复制代码
1.公平锁加锁步骤
通过以上Lua,可以发现,lua操作的关键结构是列表(list)和有序集合(zSet)。
其中list维护了一个等待的线程队列redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合redisson_lock_timeout:{xxx},尽管lua较长,但是可以拆分为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获取到锁,结束
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
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
1.等待队列有头节点
1.1未过期->2
2.不存在该锁不成立->3
3.非重入线程->4
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获取到锁,结束
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)和官方推荐的红锁(RedLock),下一章再详细介绍。
所以,当你真的需要分布式锁时,不妨先来Redisson里找找。