一、Redisson概述
二、分布式锁
三、Redisson分布式锁
四、RLock
五、公平锁
六、总结
什么是Redisson?
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),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
一个基于Redis实现的分布式工具,有基本分布式对象和高级又抽象的分布式服务,为每个试图再造分布式轮子的程序员带来了大部分分布式问题的解决办法。
Redisson和Jedis、Lettuce有什么区别?倒也不是雷锋和雷锋塔
Redisson和它俩的区别就像一个用鼠标操作图形化界面,一个用命令行操作文件。Redisson是更高层的抽象,Jedis和Lettuce是Redis命令的封装。
Jedis是Redis官方推出的用于通过Java连接Redis客户端的一个工具包,提供了Redis的各种命令支持
Lettuce是一种可扩展的线程安全的 Redis 客户端,通讯框架基于Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。
Redisson是架设在Redis基础上,通讯基于Netty的综合的、新型的中间件,企业级开发中使用Redis的最佳范本
Jedis把Redis命令封装好,Lettuce则进一步有了更丰富的Api,也支持集群等模式。但是两者也都点到为止,只给了你操作Redis数据库的脚手架,而Redisson则是基于Redis、Lua和Netty建立起了成熟的分布式解决方案,甚至redis官方都推荐的一种工具集。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 项目地址:https://github.com/YunaiV/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
分布式锁怎么实现?
分布式锁是并发业务下的刚需,虽然实现五花八门: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 计数器,记录该线程获取锁的次数 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实现分布式锁 **/ @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就有这把你要的锁。 基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 项目地址:https://github.com/YunaiV/yudao-cloud 视频教程:https://doc.iocoder.cn/video/ 三、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模型,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个步骤 1.队列清理 保证队列中只有未过期的等待线程 2.首次加锁 hset加锁,pexpire过期时间 3.重入判断 此处同可重入锁lua 4.返回ttl 5.计算尾节点ttl 初始值为锁的剩余过期时间 6.末尾排队 ttl + 2 * currentTime + waitTime是score的默认值计算公式 2.模拟 如果模拟以下顺序,就会明了redisson公平锁整个加锁流程 假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20 t1:当线程1初次获取锁 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束 t2:线程2尝试获取锁(线程1未释放锁) 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10 t3:线程3尝试获取锁(线程1未释放锁) 1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20 如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。 而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取) 10:00:30 线程2尝试获取锁(线程1已释放锁) 1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束 排队结构如图 公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。 由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。 并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 六、总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。 Redisson也提供了多机情况下的联锁MultiLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock 和官方推荐的红锁RedLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(java,redis,开发语言) 【开源】基于SSM框架“大学生艺术节”管理系统(计算机毕业设计)+万字毕业论文+远程部署+ppt+代码讲解 ssm284 计算机毕业设计_gzs 开源课程设计spring毕设mybatisjava毕业设计 系统合集跳转源码获取链接点击主页更能获取海量源码博主联系方式拉到下方点击名片获取!!!博主联系方式拉到下方点击名片获取!!!10年计算机开发经验,主营业务:源码获取、项目二开、语音辅导、远程调试、毕业设计、课程设计、毕业论文、BUG修改一、系统环境运行环境:最好是javajdk1.8,我们在这个平台上运行的。其他版本理论上也可以。IDE环境:Eclipse,Myeclipse,IDEA或者Spri 【数据序列化协议】Protocol Buffers 茉菇 网络 一、为什么需要序列化?数据跨平台/语言交互:不同编程语言(如Java、Python、Go)的数据结构不兼容,序列化提供统一的数据表示。例如:Java的HashMap和Python的dict需转换为通用格式(如JSON、Protobuf)才能通信。网络传输优化:原始内存中的对象包含指针、元数据等冗余信息,无法直接传输。序列化后数据体积更小,减少带宽占用,提升传输效率。持久化存储:将对象转换为字节流或 JavaScript 任务队列详解:Event Loop、宏任务与微任务 咖啡虫 前端中的一些概念及理解javascriptvim开发语言 JavaScript任务队列详解:EventLoop、宏任务与微任务在JavaScript的世界里,异步编程是一个至关重要的概念。JavaScript采用单线程运行方式,但能够处理异步任务,这一切都要归功于事件循环(EventLoop)机制。本文将深入剖析JavaScript的任务队列(TaskQueue),包括宏任务(Macrotask)和微任务(Microtask),并结合示例解析代码的执行顺 JavaScript函数-arguments的使用 難釋懷 javascript开发语言 在JavaScript编程语言中,函数是构建复杂逻辑和实现代码复用的关键组件。虽然现代JavaScript(尤其是ES6及之后版本)提供了更多灵活的方式来处理函数参数(如剩余参数、默认参数等),但arguments对象仍然是一个非常有用且强大的特性,尤其是在处理不定数量参数的场景中。本文将深入探讨arguments对象的使用方法及其应用场景。arguments对象简介在每个函数内部,都有一个名为a 前端 fetch API 调用 Tushare 的数据接口获取免费的基金股票信息数据 匹马夕阳 开发工具前端开源 要在前端使用JavaScript的fetchAPI调用Tushare的数据接口,您需要遵循以下步骤:1.注册Tushare账号并获取Token首先,访问Tushare官网注册账号。注册成功后,登录账号,在个人中心获取您的APIToken。2.构建请求参数Tushare的API接口采用POST请求方式,参数需要以JSON格式传递。以下是一个示例请求参数:{"api_name":"stock_basi linux下使用nohup命令不输出任何文件 Carrot_ly linux运维 nohup"command">/dev/null2>&1&"command"代表不使用nohup时的命令全部内容例:nohupjava-jaraircas-1.0.0.jar>/dev/null2>&1& Java Web开发 yourkin666 java前端开发语言 JavaWeb开发Java部分:面向对象后,注重学集合,抛异常,泛型,线程,反射,注解【Java零基础视频教程(适合Java基础,Java入门)老杜Java13版】https://www.bilibili.com/video/BV1mE411x7Wt?vd_source=4543341eea15096fa471f9067cc841ff【【零基础快速学Java】韩顺平零基础30天学会Java】htt AI赋能Spring Boot:打造智能应用的秘诀 墨瑾轩 一起学学Java【一】人工智能springboot后端 关注墨瑾轩,带你探索Java的奥秘超萌技术攻略,轻松晋级编程高手技术宝库已备好,就等你来挖掘订阅墨瑾轩,智趣学习不孤单即刻启航,编程之旅更有趣引言各位技术探险家们,欢迎来到今天的冒险——我们将一起探索如何将SpringBoot与AI服务集成,利用OpenAI和TensorFlow提升应用的智能。想象一下,你的应用程序不再是一个简单的代码集合,而是一个拥有智慧的伙伴,能够理解和预测用户的需求。这就是 Redis学习笔记——(17)Redis面试题及答案 码农小高 Redisredis学习数据库 Redis面试题1.什么是redis?Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sortedset--有序集合)和hash。这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内 Kotlin中Retrofit网络请求简单封装 weixin_34293059 移动开发java 使用Kotlin简单的对Retrofit+RxJava大多数普通请求进行封装提示:如果对Kotlin,RxLifecycle,Retrofit,RxJava等不太了解的小伙伴可以参考网上的基础资料1.惯例先添加依赖//Retrofitimplementation'com.squareup.retrofit2:retrofit:2.3.0'implementation'com.squareup.re 比Python、Java更快的 Go 语言,能否称霸江湖? weixin_33686714 pythonjavaruby 文章来源:jb51.net有一种语言堪称比语言排行榜前五热门选手的Python、Java更快,它就是GO语言。Go于2009年11月正式宣布推出,成为开放源代码项目,并在Linux及MacOSX平台上进行了实现,后来追加了Windows系统下的实现。在2016年,Go被软件评价公司TIOBE选为“TIOBE2016年最佳语言”。目前,Go每半年发布一个二级版本(即从a.x升级到a.y)。在Go语言 Java中的引用(reference)和C/C++中的指针 鱼力舟 HOT100javac语言c++ publicListNodedetectCycle(ListNodehead){HashSetlistNodes=newHashSet的作用HashSet用来存储链表中的节点(ListNode对象)。每个节点在链表中都有一个唯一的引用(即内存地址),而HashSet用来检查是否已经遍历过这个节点。在这段代码中,HashSet的add()方法返回false时,说明该节点已经存在于集合中,也就意味着我 前端数据类型终极指南:从JavaScript到TypeScript 念九_ysl typescript前端 前端数据类型全景解析.type-system{max-width:1000px;margin:0auto;padding:20px;}.type-card{border:1pxsolid#e0e0e0;padding:20px;margin:15px0;border-radius:8px;}.ts-badge{background:#3178c6;color:white;padding:2px5p Future和FutureTask实现类详解以及使用。 一个儒雅随和的男子 多线程java 前言Future是Java并发编程中的一个接口,用来表示异步计算的结果。它允许我们提交一个任务,然后之后再去获取结果,或者在结果可用时处理它。我们需要考虑Future的主要方法。根据文档,Future接口有几个关键方法:isDone()检查计算是否完成,get()获取结果(会阻塞直到完成),cancel()尝试取消任务,isCancelled()判断是否被取消。这些方法的作用和用法需要详细说明。然 Android 数据加载与分页业务 抹香鲸的弟弟蓝鲸 androidkotlinandroidjava 1,前言这篇文章写的是数据加载和分页。用到的库有'androidx.lifecycle:lifecycle-service:2.4.0''androidx.lifecycle:lifecycle-extensions:2.2.0''androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'io.reactivex.rxjava3:rxjava:3.0.4io 2.最多提取子串数目(100分)-附带Java逐行解析 最遥远的瞬间 华为OD机考试题算法 题目给定[a-z],26个英文字母小写字符串组成的字符串A和B,其中A可能存在重复字母,B不会存在重复字母,现从字符串A中按规则挑选一些字母,可以组成字符串B。挑选规则如下:同一个位置的字母只能挑选一次被挑选字母的相对先后顺序不能被改变求最多可以同时从A中挑选多少组能组成B的字符串。输入描述输入为2行,第1行输入字符串A,第2行输入字符串B,行首行尾没有多余空格,其中:A、B均由[a-z]26个英 蓝桥杯Java大学A组-15届省赛学习笔记 You_know_kho 蓝桥杯Java大学A组真题蓝桥杯java学习 蓝桥杯历届真题蓝桥杯历届真题蓝桥杯评测平台蓝桥杯模拟考试结果填空题试题A:拼正方形解题思路题目中数据很大,这并不重要,找到解法后100和100万并无本质区别。首先,如果只有一种2x2规格的正方形,那么平方数个小正方形正好可以拼出一个大正方形,例如4个拼出4x4,9个拼出6x6。其次,四个1x1规格的正方形可以拼出一个2x2规格的正方形。结合以上两点,先计算1x1规格的正方形可以拼出多少个2x2规格 DeepSeek写俄罗斯方块手机小游戏 浅痕~ 大模型游戏DeepSeek手机小游戏 DeepSeek写俄罗斯方块手机小游戏提问根据提的要求,让DeepSeek整理的需求,进行提问,内容如下:请生成一个包含以下功能的可运行移动端俄罗斯方块H5文件:核心功能要求原生JavaScript实现,适配手机屏幕支持触摸操作:左右滑动移动、点击屏幕旋转方块、往下滑方块快速下落下方显示虚拟控制按钮(左移/旋转/右移)、暂停/继续、重新开始按钮经典下落机制(自动下落、碰撞检测、行消除)右上角全屏切 Redis数据库面试——数据结构类型知识 Good Note 补档数据库redis面试服务端后端数据结构缓存 大家好,这里是GoodNote,关注公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Redis提供的5种基本数据结构类型和4种特殊类型,除此之外,还有8种底层数据结构,每种结构类型有其特点和适用场景。文章目录基本数据类型1.String(字符串)使用场景缓存计数器ID生成器分布式锁2.Hash(哈希)3.List(链表/列表)4.Set(集合)5.SortedSet(有序集合)特殊 Hutool - Http:基于 HttpUrlConnection 的 Http 客户端封装 五行星辰 业务系统应用技术hive大数据hadoopjava后端 一、简介在现代的软件开发中,与外部服务进行HTTP通信是非常常见的需求,比如调用第三方API、获取网页内容等。Java标准库中的HttpUrlConnection提供了基本的HTTP请求功能,但使用起来较为繁琐,需要处理很多细节,如连接管理、请求头设置、响应处理等。Hutool-Http模块对HttpUrlConnection进行了封装,提供了简洁易用的API,使得开发者可以更方便地进行HTTP请 Yarn 依赖解析机制深度解析:确定性安装与版本冲突的工程哲学 引言:包管理工具的"圣杯问题"在现代前端工程中,依赖管理已成为构建稳定性的核心挑战。根据2023年JavaScript生态调查报告显示,平均每个前端项目依赖1,200+个第三方包,嵌套依赖层级超过15层。在这样的复杂度下,如何实现确定性安装(DeterministicInstallation)和版本冲突智能解决,成为Yarn这类包管理工具的核心战场。本文将深入探讨Yarn(特别是Classicv1 Spring Bean 如何保证并发安全??? G丶AEOM 八股普通学习区java八股spring SpringBean如何保证并发安全简单来说:1、可以设置Beon的作用域为原型,这样每次从容器中获取该Bean时,都会创建一个新的实例,避免了多线程共享同一个对象实例的问题2、在不改变Beon的作用域的情况下,可以避免在Beon中存在可变状态的声明,尽量将状态信息存在方法内部的局部变量中,或者使用线程安全的数据结构,如ConcurrentHashMap来管理状态3、使用Java并发编程中提供的锁 leetcode 205. 同构字符串-java实现 依嘫_吃代码 LeetCodeleetcodejava算法 题目所属分类华为校招原题链接给定两个字符串s和t,判断它们是否是同构的。如果s中的字符可以按某种映射关系替换得到t,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。代码案例:输入:s=“egg”,t=“add”输出:true输入:s=“foo”,t=“bar”输出:fals ssm神马物流+vue 源码哆哆V+ymhydo Java毕设优质源码vue.js前端javascript 摘要本神马物流管理系统设计目标是实现神马物流的信息化管理,提高管理效率,使得神马物流管理作规范化、科学化、高效化。本文重点阐述了神马物流管理系统的开发过程,以实际运用为开发背景,基于SSM+Vue框架,运用了Java编程语言和MYSQL数据库进行开发,充分保证系统的安全性和稳定性。本系统界面良好,操作简单方便,通过系统概述、系统分析、系统设计、数据库设计、系统测试这几个部分,详细的说明了系统的开发 2025蓝桥杯JAVA编程题练习Day4 起床悠悠 Java蓝桥杯算法学习蓝桥杯职场和发展java算法 1.艺术与篮球问题描述小蓝出生在一个艺术与运动并重的家庭中。妈妈是位书法家,她希望小蓝能通过练习书法,继承她的艺术天赋,并练就一手好字。爸爸是一名篮球教练,他希望小蓝能通过篮球锻炼身体,培养运动的激情和团队合作的精神。为了既满足妈妈的期望,又不辜负爸爸的心意,小蓝决定根据日期的笔画数来安排自己的练习。首先,他会将当天的日期按照“YYYYMMDD”的格式转换成一个8位数,然后将这8位数对应到汉字上, 【华为OD技术面试手撕真题】98、同构字符串 | 手撕真题+思路参考+代码解析(C & C++ & Java & Python & JS)(0ms) KJ.JK 华为OD技术面试手撕真题华为od面试c语言华为od机试真题华为od机试E卷javascript同构字符串 文章目录一、题目题目描述样例1二、代码参考C语言思路C语言代码C++语言思路C++代码Java语言思路Java代码Python语言思路Python代码JS语言思路JS代码作者:KJ.JK个人博客首页:KJ.JK专栏介绍:本专栏更新每年华为OD机试的高频手撕代码题,每个题目都会使用五种语言进行解答(C&C++&Java&Python&JS),思路分析都非常详细,争取实现最低的时间复杂度和高通过率,每 2025蓝桥杯JAVA编程题练习Day5 起床悠悠 蓝桥杯Java算法学习算法数据结构java蓝桥杯 1.最少步数【省模拟赛】问题描述小蓝要上一个楼梯,楼梯共有n级台阶(即小蓝总共要走nn级)。小蓝每一步可以走1级、2级或3级台阶。请问小蓝至少要多少步才能上到楼梯顶端?输入格式输入一行包含一个整数n。输出格式输出一行包含一个整数,表示答案。样例输入19样例输出13样例输入210样例输出24AC代码importjava.util.*;publicclassexercise1{staticScanne redis sentinel配置“sentinel parallel-syncs mymaster 1“详解 学会了没 redissentinelbootstrap 在RedisSentinel的配置中,sentinelparallel-syncsmymaster1这一行配置的作用是控制故障转移(Failover)后,允许同时向新主节点(NewMaster)发起数据同步的从节点(Slave)数量。下面通过场景示例详细解释:参数定义配置格式:sentinelparallel-syncs:监控的主节点名称(如mymaster)。:允许同时同步的从节点数量。默认值: Redis 常用的五种数据类型 HinINAX redischrome数据库 Redis常用的五种数据类型一、字符串(String)简介字符串是Redis中最基本的数据类型,可以存储任何类型的数据(如文本、数字、二进制数据等)。每个字符串的最大长度可以达到512MB。常用命令SETkeyvalue用途:设置键key的值为value。语法:SETkeyvalue示例:SETusername"john_doe"GETkey用途:获取键key的值。语法:GETkey示例:GETu java后端开发day19--学生管理系统升级 元亓亓亓 java后端开发java开发语言 (以下内容全部来自上述课程)1.要求及思路1.总体框架2.注册3.登录4.忘记密码2.代码1.javabeanpublicclassUser1{privateStringusername;privateStringpassword;privateStringpersonID;privateStringphoneNumber;publicUser1(){}publicUser1(Stringuser ztree设置禁用节点 3213213333332132 JavaScriptztreejsonsetDisabledNodeAjax ztree设置禁用节点的时候注意,当使用ajax后台请求数据,必须要设置为同步获取数据,否者会获取不到节点对象,导致设置禁用没有效果。 $(function(){ showTree(); setDisabledNode(); }); JVM patch by Taobao bookjovi javaHotSpot 在网上无意中看到淘宝提交的hotspot patch,共四个,有意思,记录一下。 7050685:jsdbproc64.sh has a typo in the package name 7058036:FieldsAllocationStyle=2 does not work in 32-bit VM 7060619:C1 should respect inline and 将session存储到数据库中 dcj3sjt126com sqlPHPsession CREATE TABLE sessions ( id CHAR(32) NOT NULL, data TEXT, last_accessed TIMESTAMP NOT NULL, PRIMARY KEY (id) ); <?php /** * Created by PhpStorm. * User: michaeldu * Date Vector 171815164 vector public Vector<CartProduct> delCart(Vector<CartProduct> cart, String id) { for (int i = 0; i < cart.size(); i++) { if (cart.get(i).getId().equals(id)) { cart.remove(i); 各连接池配置参数比较 g21121 连接池 排版真心费劲,大家凑合看下吧,见谅~ Druid DBCP C3P0 Proxool 数据库用户名称 Username Username User 数据库密码 Password Password Password 驱动名 [简单]mybatis insert语句添加动态字段 53873039oycg mybatis mysql数据库,id自增,配置如下: <insert id="saveTestTb" useGeneratedKeys="true" keyProperty="id" parameterType=& struts2拦截器配置 云端月影 struts2拦截器 struts2拦截器interceptor的三种配置方法 方法1. 普通配置法 <struts> <package name="struts2" extends="struts-default"> & IE中页面不居中,火狐谷歌等正常 aijuans IE中页面不居中 问题是首页在火狐、谷歌、所有IE中正常显示,列表页的页面在火狐谷歌中正常,在IE6、7、8中都不中,觉得可能那个地方设置的让IE系列都不认识,仔细查看后发现,列表页中没写HTML模板部分没有添加DTD定义,就是<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3 String,int,Integer,char 几个类型常见转换 antonyup_2006 htmlsql.net 如何将字串 String 转换成整数 int? int i = Integer.valueOf(my_str).intValue(); int i=Integer.parseInt(str); 如何将字串 String 转换成Integer ? Integer integer=Integer.valueOf(str); 如何将整数 int 转换成字串 String ? 1. PL/SQL的游标类型 百合不是茶 显示游标(静态游标)隐式游标游标的更新和删除%rowtyperef游标(动态游标) 游标是oracle中的一个结果集,用于存放查询的结果; PL/SQL中游标的声明; 1,声明游标 2,打开游标(默认是关闭的); 3,提取数据 4,关闭游标 注意的要点:游标必须声明在declare中,使用open打开游标,fetch取游标中的数据,close关闭游标 隐式游标:主要是对DML数据的操作隐 JUnit4中@AfterClass @BeforeClass @after @before的区别对比 bijian1013 JUnit4单元测试 一.基础知识 JUnit4使用Java5中的注解(annotation),以下是JUnit4常用的几个annotation: @Before:初始化方法 对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)@After:释放资源 对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次 精通Oracle10编程SQL(12)开发包 bijian1013 oracle数据库plsql /* *开发包 *包用于逻辑组合相关的PL/SQL类型(例如TABLE类型和RECORD类型)、PL/SQL项(例如游标和游标变量)和PL/SQL子程序(例如过程和函数) */ --包用于逻辑组合相关的PL/SQL类型、项和子程序,它由包规范和包体两部分组成 --建立包规范:包规范实际是包与应用程序之间的接口,它用于定义包的公用组件,包括常量、变量、游标、过程和函数等 --在包规 【EhCache二】ehcache.xml配置详解 bit1129 ehcache.xml 在ehcache官网上找了多次,终于找到ehcache.xml配置元素和属性的含义说明文档了,这个文档包含在ehcache.xml的注释中! ehcache.xml : http://ehcache.org/ehcache.xml ehcache.xsd : http://ehcache.org/ehcache.xsd ehcache配置文件的根元素是ehcahe ehcac java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderL 白糖_ javaeclipsespringtomcatWeb 今天学习spring+cxf的时候遇到一个问题:在web.xml中配置了spring的上下文监听器: <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> 随后启动 angular.element boyitech AngularJSAngularJS APIangular.element angular.element 描述: 包裹着一部分DOM element或者是HTML字符串,把它作为一个jQuery元素来处理。(类似于jQuery的选择器啦) 如果jQuery被引入了,则angular.element就可以看作是jQuery选择器,选择的对象可以使用jQuery的函数;如果jQuery不可用,angular.e java-给定两个已排序序列,找出共同的元素。 bylijinnan java import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class CommonItemInTwoSortedArray { /** * 题目:给定两个已排序序列,找出共同的元素。 * 1.定义两个指针分别指向序列的开始。 * 如果指向的两个元素 sftp 异常,有遇到的吗?求解 Chen.H javajcraftauthjschjschexception com.jcraft.jsch.JSchException: Auth cancel at com.jcraft.jsch.Session.connect(Session.java:460) at com.jcraft.jsch.Session.connect(Session.java:154) at cn.vivame.util.ftp.SftpServerAccess.connec [生物智能与人工智能]神经元中的电化学结构代表什么? comsci 人工智能 我这里做一个大胆的猜想,生物神经网络中的神经元中包含着一些化学和类似电路的结构,这些结构通常用来扮演类似我们在拓扑分析系统中的节点嵌入方程一样,使得我们的神经网络产生智能判断的能力,而这些嵌入到节点中的方程同时也扮演着"经验"的角色.... 我们可以尝试一下...在某些神经 通过LAC和CID获取经纬度信息 dai_lm laccid 方法1: 用浏览器打开http://www.minigps.net/cellsearch.html,然后输入lac和cid信息(mcc和mnc可以填0),如果数据正确就可以获得相应的经纬度 方法2: 发送HTTP请求到http://www.open-electronics.org/celltrack/cell.php?hex=0&lac=<lac>&cid=& JAVA的困难分析 datamachine java 前段时间转了一篇SQL的文章(http://datamachine.iteye.com/blog/1971896),文章不复杂,但思想深刻,就顺便思考了一下java的不足,当砖头丢出来,希望引点和田玉。 ----------------------------------------------------------------------------------------- 小学5年级英语单词背诵第二课 dcj3sjt126com englishword money 钱 paper 纸 speak 讲,说 tell 告诉 remember 记得,想起 knock 敲,击,打 question 问题 number 数字,号码 learn 学会,学习 street 街道 carry 搬运,携带 send 发送,邮寄,发射 must 必须 light 灯,光线,轻的 front linux下面没有tree命令 dcj3sjt126com linux centos p安装 yum -y install tree mac os安装 brew install tree 首先来看tree的用法 tree 中文解释:tree 功能说明:以树状图列出目录的内容。 语 法:tree [-aACdDfFgilnNpqstux][-I <范本样式>][-P <范本样式 Map迭代方式,Map迭代,Map循环 蕃薯耀 Map循环Map迭代Map迭代方式 Map迭代方式,Map迭代,Map循环 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 蕃薯耀 2015年 Spring Cache注解+Redis hanqunfeng spring Spring3.1 Cache注解 依赖jar包: <!-- redis --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> Guava中针对集合的 filter和过滤功能 jackyrong filter 在guava库中,自带了过滤器(filter)的功能,可以用来对collection 进行过滤,先看例子: @Test public void whenFilterWithIterables_thenFiltered() { List<String> names = Lists.newArrayList("John" 学习编程那点事 lampcy 编程androidPHPhtml5 一年前的夏天,我还在纠结要不要改行,要不要去学php?能学到真本事吗?改行能成功吗?太多的问题,我终于不顾一切,下定决心,辞去了工作,来到传说中的帝都。老师给的乘车方式还算有效,很顺利的就到了学校,赶巧了,正好学校搬到了新校区。先安顿了下来,过了个轻松的周末,第一次到帝都,逛逛吧! 接下来的周一,是我噩梦的开始,学习内容对我这个零基础的人来说,除了勉强完成老师布置的作业外,我已经没有时间和精力去 架构师之流处理---------bytebuffer的mark,limit和flip nannan408 ByteBuffer 1.前言。 如题,limit其实就是可以读取的字节长度的意思,flip是清空的意思,mark是标记的意思 。 2.例子. 例子代码: String str = "helloWorld"; ByteBuffer buff = ByteBuffer.wrap(str.getBytes()); Sy org.apache.el.parser.ParseException: Encountered " ":" ": "" at line 1, column 1 Everyday都不同 $转义el表达式 最近在做Highcharts的过程中,在写js时,出现了以下异常: 严重: Servlet.service() for servlet jsp threw exception org.apache.el.parser.ParseException: Encountered " ":" ": "" at line 1, 用Java实现发送邮件到163 tntxia java实现 /* 在java版经常看到有人问如何用javamail发送邮件?如何接收邮件?如何访问多个文件夹等。问题零散,而历史的回复早已经淹没在问题的海洋之中。 本人之前所做过一个java项目,其中包含有WebMail功能,当初为用java实现而对javamail摸索了一段时间,总算有点收获。看到论坛中的经常有此方面的问题,因此把我的一些经验帖出来,希望对大家有些帮助。 此篇仅介绍用 探索实体类存在的真正意义 java小叶檀 POJO 一. 实体类简述 实体类其实就是俗称的POJO,这种类一般不实现特殊框架下的接口,在程序中仅作为数据容器用来持久化存储数据用的 POJO(Plain Old Java Objects)简单的Java对象 它的一般格式就是 public class A{ private String id; public Str 按字母分类: 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 计数器,记录该线程获取锁的次数
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实现分布式锁 **/ @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就有这把你要的锁。 基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 项目地址:https://github.com/YunaiV/yudao-cloud 视频教程:https://doc.iocoder.cn/video/ 三、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模型,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个步骤 1.队列清理 保证队列中只有未过期的等待线程 2.首次加锁 hset加锁,pexpire过期时间 3.重入判断 此处同可重入锁lua 4.返回ttl 5.计算尾节点ttl 初始值为锁的剩余过期时间 6.末尾排队 ttl + 2 * currentTime + waitTime是score的默认值计算公式 2.模拟 如果模拟以下顺序,就会明了redisson公平锁整个加锁流程 假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20 t1:当线程1初次获取锁 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束 t2:线程2尝试获取锁(线程1未释放锁) 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10 t3:线程3尝试获取锁(线程1未释放锁) 1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20 如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。 而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取) 10:00:30 线程2尝试获取锁(线程1已释放锁) 1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束 排队结构如图 公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。 由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。 并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 六、总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。 Redisson也提供了多机情况下的联锁MultiLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock 和官方推荐的红锁RedLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(java,redis,开发语言) 【开源】基于SSM框架“大学生艺术节”管理系统(计算机毕业设计)+万字毕业论文+远程部署+ppt+代码讲解 ssm284 计算机毕业设计_gzs 开源课程设计spring毕设mybatisjava毕业设计 系统合集跳转源码获取链接点击主页更能获取海量源码博主联系方式拉到下方点击名片获取!!!博主联系方式拉到下方点击名片获取!!!10年计算机开发经验,主营业务:源码获取、项目二开、语音辅导、远程调试、毕业设计、课程设计、毕业论文、BUG修改一、系统环境运行环境:最好是javajdk1.8,我们在这个平台上运行的。其他版本理论上也可以。IDE环境:Eclipse,Myeclipse,IDEA或者Spri 【数据序列化协议】Protocol Buffers 茉菇 网络 一、为什么需要序列化?数据跨平台/语言交互:不同编程语言(如Java、Python、Go)的数据结构不兼容,序列化提供统一的数据表示。例如:Java的HashMap和Python的dict需转换为通用格式(如JSON、Protobuf)才能通信。网络传输优化:原始内存中的对象包含指针、元数据等冗余信息,无法直接传输。序列化后数据体积更小,减少带宽占用,提升传输效率。持久化存储:将对象转换为字节流或 JavaScript 任务队列详解:Event Loop、宏任务与微任务 咖啡虫 前端中的一些概念及理解javascriptvim开发语言 JavaScript任务队列详解:EventLoop、宏任务与微任务在JavaScript的世界里,异步编程是一个至关重要的概念。JavaScript采用单线程运行方式,但能够处理异步任务,这一切都要归功于事件循环(EventLoop)机制。本文将深入剖析JavaScript的任务队列(TaskQueue),包括宏任务(Macrotask)和微任务(Microtask),并结合示例解析代码的执行顺 JavaScript函数-arguments的使用 難釋懷 javascript开发语言 在JavaScript编程语言中,函数是构建复杂逻辑和实现代码复用的关键组件。虽然现代JavaScript(尤其是ES6及之后版本)提供了更多灵活的方式来处理函数参数(如剩余参数、默认参数等),但arguments对象仍然是一个非常有用且强大的特性,尤其是在处理不定数量参数的场景中。本文将深入探讨arguments对象的使用方法及其应用场景。arguments对象简介在每个函数内部,都有一个名为a 前端 fetch API 调用 Tushare 的数据接口获取免费的基金股票信息数据 匹马夕阳 开发工具前端开源 要在前端使用JavaScript的fetchAPI调用Tushare的数据接口,您需要遵循以下步骤:1.注册Tushare账号并获取Token首先,访问Tushare官网注册账号。注册成功后,登录账号,在个人中心获取您的APIToken。2.构建请求参数Tushare的API接口采用POST请求方式,参数需要以JSON格式传递。以下是一个示例请求参数:{"api_name":"stock_basi linux下使用nohup命令不输出任何文件 Carrot_ly linux运维 nohup"command">/dev/null2>&1&"command"代表不使用nohup时的命令全部内容例:nohupjava-jaraircas-1.0.0.jar>/dev/null2>&1& Java Web开发 yourkin666 java前端开发语言 JavaWeb开发Java部分:面向对象后,注重学集合,抛异常,泛型,线程,反射,注解【Java零基础视频教程(适合Java基础,Java入门)老杜Java13版】https://www.bilibili.com/video/BV1mE411x7Wt?vd_source=4543341eea15096fa471f9067cc841ff【【零基础快速学Java】韩顺平零基础30天学会Java】htt AI赋能Spring Boot:打造智能应用的秘诀 墨瑾轩 一起学学Java【一】人工智能springboot后端 关注墨瑾轩,带你探索Java的奥秘超萌技术攻略,轻松晋级编程高手技术宝库已备好,就等你来挖掘订阅墨瑾轩,智趣学习不孤单即刻启航,编程之旅更有趣引言各位技术探险家们,欢迎来到今天的冒险——我们将一起探索如何将SpringBoot与AI服务集成,利用OpenAI和TensorFlow提升应用的智能。想象一下,你的应用程序不再是一个简单的代码集合,而是一个拥有智慧的伙伴,能够理解和预测用户的需求。这就是 Redis学习笔记——(17)Redis面试题及答案 码农小高 Redisredis学习数据库 Redis面试题1.什么是redis?Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sortedset--有序集合)和hash。这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内 Kotlin中Retrofit网络请求简单封装 weixin_34293059 移动开发java 使用Kotlin简单的对Retrofit+RxJava大多数普通请求进行封装提示:如果对Kotlin,RxLifecycle,Retrofit,RxJava等不太了解的小伙伴可以参考网上的基础资料1.惯例先添加依赖//Retrofitimplementation'com.squareup.retrofit2:retrofit:2.3.0'implementation'com.squareup.re 比Python、Java更快的 Go 语言,能否称霸江湖? weixin_33686714 pythonjavaruby 文章来源:jb51.net有一种语言堪称比语言排行榜前五热门选手的Python、Java更快,它就是GO语言。Go于2009年11月正式宣布推出,成为开放源代码项目,并在Linux及MacOSX平台上进行了实现,后来追加了Windows系统下的实现。在2016年,Go被软件评价公司TIOBE选为“TIOBE2016年最佳语言”。目前,Go每半年发布一个二级版本(即从a.x升级到a.y)。在Go语言 Java中的引用(reference)和C/C++中的指针 鱼力舟 HOT100javac语言c++ publicListNodedetectCycle(ListNodehead){HashSetlistNodes=newHashSet的作用HashSet用来存储链表中的节点(ListNode对象)。每个节点在链表中都有一个唯一的引用(即内存地址),而HashSet用来检查是否已经遍历过这个节点。在这段代码中,HashSet的add()方法返回false时,说明该节点已经存在于集合中,也就意味着我 前端数据类型终极指南:从JavaScript到TypeScript 念九_ysl typescript前端 前端数据类型全景解析.type-system{max-width:1000px;margin:0auto;padding:20px;}.type-card{border:1pxsolid#e0e0e0;padding:20px;margin:15px0;border-radius:8px;}.ts-badge{background:#3178c6;color:white;padding:2px5p Future和FutureTask实现类详解以及使用。 一个儒雅随和的男子 多线程java 前言Future是Java并发编程中的一个接口,用来表示异步计算的结果。它允许我们提交一个任务,然后之后再去获取结果,或者在结果可用时处理它。我们需要考虑Future的主要方法。根据文档,Future接口有几个关键方法:isDone()检查计算是否完成,get()获取结果(会阻塞直到完成),cancel()尝试取消任务,isCancelled()判断是否被取消。这些方法的作用和用法需要详细说明。然 Android 数据加载与分页业务 抹香鲸的弟弟蓝鲸 androidkotlinandroidjava 1,前言这篇文章写的是数据加载和分页。用到的库有'androidx.lifecycle:lifecycle-service:2.4.0''androidx.lifecycle:lifecycle-extensions:2.2.0''androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'io.reactivex.rxjava3:rxjava:3.0.4io 2.最多提取子串数目(100分)-附带Java逐行解析 最遥远的瞬间 华为OD机考试题算法 题目给定[a-z],26个英文字母小写字符串组成的字符串A和B,其中A可能存在重复字母,B不会存在重复字母,现从字符串A中按规则挑选一些字母,可以组成字符串B。挑选规则如下:同一个位置的字母只能挑选一次被挑选字母的相对先后顺序不能被改变求最多可以同时从A中挑选多少组能组成B的字符串。输入描述输入为2行,第1行输入字符串A,第2行输入字符串B,行首行尾没有多余空格,其中:A、B均由[a-z]26个英 蓝桥杯Java大学A组-15届省赛学习笔记 You_know_kho 蓝桥杯Java大学A组真题蓝桥杯java学习 蓝桥杯历届真题蓝桥杯历届真题蓝桥杯评测平台蓝桥杯模拟考试结果填空题试题A:拼正方形解题思路题目中数据很大,这并不重要,找到解法后100和100万并无本质区别。首先,如果只有一种2x2规格的正方形,那么平方数个小正方形正好可以拼出一个大正方形,例如4个拼出4x4,9个拼出6x6。其次,四个1x1规格的正方形可以拼出一个2x2规格的正方形。结合以上两点,先计算1x1规格的正方形可以拼出多少个2x2规格 DeepSeek写俄罗斯方块手机小游戏 浅痕~ 大模型游戏DeepSeek手机小游戏 DeepSeek写俄罗斯方块手机小游戏提问根据提的要求,让DeepSeek整理的需求,进行提问,内容如下:请生成一个包含以下功能的可运行移动端俄罗斯方块H5文件:核心功能要求原生JavaScript实现,适配手机屏幕支持触摸操作:左右滑动移动、点击屏幕旋转方块、往下滑方块快速下落下方显示虚拟控制按钮(左移/旋转/右移)、暂停/继续、重新开始按钮经典下落机制(自动下落、碰撞检测、行消除)右上角全屏切 Redis数据库面试——数据结构类型知识 Good Note 补档数据库redis面试服务端后端数据结构缓存 大家好,这里是GoodNote,关注公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Redis提供的5种基本数据结构类型和4种特殊类型,除此之外,还有8种底层数据结构,每种结构类型有其特点和适用场景。文章目录基本数据类型1.String(字符串)使用场景缓存计数器ID生成器分布式锁2.Hash(哈希)3.List(链表/列表)4.Set(集合)5.SortedSet(有序集合)特殊 Hutool - Http:基于 HttpUrlConnection 的 Http 客户端封装 五行星辰 业务系统应用技术hive大数据hadoopjava后端 一、简介在现代的软件开发中,与外部服务进行HTTP通信是非常常见的需求,比如调用第三方API、获取网页内容等。Java标准库中的HttpUrlConnection提供了基本的HTTP请求功能,但使用起来较为繁琐,需要处理很多细节,如连接管理、请求头设置、响应处理等。Hutool-Http模块对HttpUrlConnection进行了封装,提供了简洁易用的API,使得开发者可以更方便地进行HTTP请 Yarn 依赖解析机制深度解析:确定性安装与版本冲突的工程哲学 引言:包管理工具的"圣杯问题"在现代前端工程中,依赖管理已成为构建稳定性的核心挑战。根据2023年JavaScript生态调查报告显示,平均每个前端项目依赖1,200+个第三方包,嵌套依赖层级超过15层。在这样的复杂度下,如何实现确定性安装(DeterministicInstallation)和版本冲突智能解决,成为Yarn这类包管理工具的核心战场。本文将深入探讨Yarn(特别是Classicv1 Spring Bean 如何保证并发安全??? G丶AEOM 八股普通学习区java八股spring SpringBean如何保证并发安全简单来说:1、可以设置Beon的作用域为原型,这样每次从容器中获取该Bean时,都会创建一个新的实例,避免了多线程共享同一个对象实例的问题2、在不改变Beon的作用域的情况下,可以避免在Beon中存在可变状态的声明,尽量将状态信息存在方法内部的局部变量中,或者使用线程安全的数据结构,如ConcurrentHashMap来管理状态3、使用Java并发编程中提供的锁 leetcode 205. 同构字符串-java实现 依嘫_吃代码 LeetCodeleetcodejava算法 题目所属分类华为校招原题链接给定两个字符串s和t,判断它们是否是同构的。如果s中的字符可以按某种映射关系替换得到t,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。代码案例:输入:s=“egg”,t=“add”输出:true输入:s=“foo”,t=“bar”输出:fals ssm神马物流+vue 源码哆哆V+ymhydo Java毕设优质源码vue.js前端javascript 摘要本神马物流管理系统设计目标是实现神马物流的信息化管理,提高管理效率,使得神马物流管理作规范化、科学化、高效化。本文重点阐述了神马物流管理系统的开发过程,以实际运用为开发背景,基于SSM+Vue框架,运用了Java编程语言和MYSQL数据库进行开发,充分保证系统的安全性和稳定性。本系统界面良好,操作简单方便,通过系统概述、系统分析、系统设计、数据库设计、系统测试这几个部分,详细的说明了系统的开发 2025蓝桥杯JAVA编程题练习Day4 起床悠悠 Java蓝桥杯算法学习蓝桥杯职场和发展java算法 1.艺术与篮球问题描述小蓝出生在一个艺术与运动并重的家庭中。妈妈是位书法家,她希望小蓝能通过练习书法,继承她的艺术天赋,并练就一手好字。爸爸是一名篮球教练,他希望小蓝能通过篮球锻炼身体,培养运动的激情和团队合作的精神。为了既满足妈妈的期望,又不辜负爸爸的心意,小蓝决定根据日期的笔画数来安排自己的练习。首先,他会将当天的日期按照“YYYYMMDD”的格式转换成一个8位数,然后将这8位数对应到汉字上, 【华为OD技术面试手撕真题】98、同构字符串 | 手撕真题+思路参考+代码解析(C & C++ & Java & Python & JS)(0ms) KJ.JK 华为OD技术面试手撕真题华为od面试c语言华为od机试真题华为od机试E卷javascript同构字符串 文章目录一、题目题目描述样例1二、代码参考C语言思路C语言代码C++语言思路C++代码Java语言思路Java代码Python语言思路Python代码JS语言思路JS代码作者:KJ.JK个人博客首页:KJ.JK专栏介绍:本专栏更新每年华为OD机试的高频手撕代码题,每个题目都会使用五种语言进行解答(C&C++&Java&Python&JS),思路分析都非常详细,争取实现最低的时间复杂度和高通过率,每 2025蓝桥杯JAVA编程题练习Day5 起床悠悠 蓝桥杯Java算法学习算法数据结构java蓝桥杯 1.最少步数【省模拟赛】问题描述小蓝要上一个楼梯,楼梯共有n级台阶(即小蓝总共要走nn级)。小蓝每一步可以走1级、2级或3级台阶。请问小蓝至少要多少步才能上到楼梯顶端?输入格式输入一行包含一个整数n。输出格式输出一行包含一个整数,表示答案。样例输入19样例输出13样例输入210样例输出24AC代码importjava.util.*;publicclassexercise1{staticScanne redis sentinel配置“sentinel parallel-syncs mymaster 1“详解 学会了没 redissentinelbootstrap 在RedisSentinel的配置中,sentinelparallel-syncsmymaster1这一行配置的作用是控制故障转移(Failover)后,允许同时向新主节点(NewMaster)发起数据同步的从节点(Slave)数量。下面通过场景示例详细解释:参数定义配置格式:sentinelparallel-syncs:监控的主节点名称(如mymaster)。:允许同时同步的从节点数量。默认值: Redis 常用的五种数据类型 HinINAX redischrome数据库 Redis常用的五种数据类型一、字符串(String)简介字符串是Redis中最基本的数据类型,可以存储任何类型的数据(如文本、数字、二进制数据等)。每个字符串的最大长度可以达到512MB。常用命令SETkeyvalue用途:设置键key的值为value。语法:SETkeyvalue示例:SETusername"john_doe"GETkey用途:获取键key的值。语法:GETkey示例:GETu java后端开发day19--学生管理系统升级 元亓亓亓 java后端开发java开发语言 (以下内容全部来自上述课程)1.要求及思路1.总体框架2.注册3.登录4.忘记密码2.代码1.javabeanpublicclassUser1{privateStringusername;privateStringpassword;privateStringpersonID;privateStringphoneNumber;publicUser1(){}publicUser1(Stringuser ztree设置禁用节点 3213213333332132 JavaScriptztreejsonsetDisabledNodeAjax ztree设置禁用节点的时候注意,当使用ajax后台请求数据,必须要设置为同步获取数据,否者会获取不到节点对象,导致设置禁用没有效果。 $(function(){ showTree(); setDisabledNode(); }); JVM patch by Taobao bookjovi javaHotSpot 在网上无意中看到淘宝提交的hotspot patch,共四个,有意思,记录一下。 7050685:jsdbproc64.sh has a typo in the package name 7058036:FieldsAllocationStyle=2 does not work in 32-bit VM 7060619:C1 should respect inline and 将session存储到数据库中 dcj3sjt126com sqlPHPsession CREATE TABLE sessions ( id CHAR(32) NOT NULL, data TEXT, last_accessed TIMESTAMP NOT NULL, PRIMARY KEY (id) ); <?php /** * Created by PhpStorm. * User: michaeldu * Date Vector 171815164 vector public Vector<CartProduct> delCart(Vector<CartProduct> cart, String id) { for (int i = 0; i < cart.size(); i++) { if (cart.get(i).getId().equals(id)) { cart.remove(i); 各连接池配置参数比较 g21121 连接池 排版真心费劲,大家凑合看下吧,见谅~ Druid DBCP C3P0 Proxool 数据库用户名称 Username Username User 数据库密码 Password Password Password 驱动名 [简单]mybatis insert语句添加动态字段 53873039oycg mybatis mysql数据库,id自增,配置如下: <insert id="saveTestTb" useGeneratedKeys="true" keyProperty="id" parameterType=& struts2拦截器配置 云端月影 struts2拦截器 struts2拦截器interceptor的三种配置方法 方法1. 普通配置法 <struts> <package name="struts2" extends="struts-default"> & IE中页面不居中,火狐谷歌等正常 aijuans IE中页面不居中 问题是首页在火狐、谷歌、所有IE中正常显示,列表页的页面在火狐谷歌中正常,在IE6、7、8中都不中,觉得可能那个地方设置的让IE系列都不认识,仔细查看后发现,列表页中没写HTML模板部分没有添加DTD定义,就是<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3 String,int,Integer,char 几个类型常见转换 antonyup_2006 htmlsql.net 如何将字串 String 转换成整数 int? int i = Integer.valueOf(my_str).intValue(); int i=Integer.parseInt(str); 如何将字串 String 转换成Integer ? Integer integer=Integer.valueOf(str); 如何将整数 int 转换成字串 String ? 1. PL/SQL的游标类型 百合不是茶 显示游标(静态游标)隐式游标游标的更新和删除%rowtyperef游标(动态游标) 游标是oracle中的一个结果集,用于存放查询的结果; PL/SQL中游标的声明; 1,声明游标 2,打开游标(默认是关闭的); 3,提取数据 4,关闭游标 注意的要点:游标必须声明在declare中,使用open打开游标,fetch取游标中的数据,close关闭游标 隐式游标:主要是对DML数据的操作隐 JUnit4中@AfterClass @BeforeClass @after @before的区别对比 bijian1013 JUnit4单元测试 一.基础知识 JUnit4使用Java5中的注解(annotation),以下是JUnit4常用的几个annotation: @Before:初始化方法 对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)@After:释放资源 对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次 精通Oracle10编程SQL(12)开发包 bijian1013 oracle数据库plsql /* *开发包 *包用于逻辑组合相关的PL/SQL类型(例如TABLE类型和RECORD类型)、PL/SQL项(例如游标和游标变量)和PL/SQL子程序(例如过程和函数) */ --包用于逻辑组合相关的PL/SQL类型、项和子程序,它由包规范和包体两部分组成 --建立包规范:包规范实际是包与应用程序之间的接口,它用于定义包的公用组件,包括常量、变量、游标、过程和函数等 --在包规 【EhCache二】ehcache.xml配置详解 bit1129 ehcache.xml 在ehcache官网上找了多次,终于找到ehcache.xml配置元素和属性的含义说明文档了,这个文档包含在ehcache.xml的注释中! ehcache.xml : http://ehcache.org/ehcache.xml ehcache.xsd : http://ehcache.org/ehcache.xsd ehcache配置文件的根元素是ehcahe ehcac java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderL 白糖_ javaeclipsespringtomcatWeb 今天学习spring+cxf的时候遇到一个问题:在web.xml中配置了spring的上下文监听器: <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> 随后启动 angular.element boyitech AngularJSAngularJS APIangular.element angular.element 描述: 包裹着一部分DOM element或者是HTML字符串,把它作为一个jQuery元素来处理。(类似于jQuery的选择器啦) 如果jQuery被引入了,则angular.element就可以看作是jQuery选择器,选择的对象可以使用jQuery的函数;如果jQuery不可用,angular.e java-给定两个已排序序列,找出共同的元素。 bylijinnan java import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class CommonItemInTwoSortedArray { /** * 题目:给定两个已排序序列,找出共同的元素。 * 1.定义两个指针分别指向序列的开始。 * 如果指向的两个元素 sftp 异常,有遇到的吗?求解 Chen.H javajcraftauthjschjschexception com.jcraft.jsch.JSchException: Auth cancel at com.jcraft.jsch.Session.connect(Session.java:460) at com.jcraft.jsch.Session.connect(Session.java:154) at cn.vivame.util.ftp.SftpServerAccess.connec [生物智能与人工智能]神经元中的电化学结构代表什么? comsci 人工智能 我这里做一个大胆的猜想,生物神经网络中的神经元中包含着一些化学和类似电路的结构,这些结构通常用来扮演类似我们在拓扑分析系统中的节点嵌入方程一样,使得我们的神经网络产生智能判断的能力,而这些嵌入到节点中的方程同时也扮演着"经验"的角色.... 我们可以尝试一下...在某些神经 通过LAC和CID获取经纬度信息 dai_lm laccid 方法1: 用浏览器打开http://www.minigps.net/cellsearch.html,然后输入lac和cid信息(mcc和mnc可以填0),如果数据正确就可以获得相应的经纬度 方法2: 发送HTTP请求到http://www.open-electronics.org/celltrack/cell.php?hex=0&lac=<lac>&cid=& JAVA的困难分析 datamachine java 前段时间转了一篇SQL的文章(http://datamachine.iteye.com/blog/1971896),文章不复杂,但思想深刻,就顺便思考了一下java的不足,当砖头丢出来,希望引点和田玉。 ----------------------------------------------------------------------------------------- 小学5年级英语单词背诵第二课 dcj3sjt126com englishword money 钱 paper 纸 speak 讲,说 tell 告诉 remember 记得,想起 knock 敲,击,打 question 问题 number 数字,号码 learn 学会,学习 street 街道 carry 搬运,携带 send 发送,邮寄,发射 must 必须 light 灯,光线,轻的 front linux下面没有tree命令 dcj3sjt126com linux centos p安装 yum -y install tree mac os安装 brew install tree 首先来看tree的用法 tree 中文解释:tree 功能说明:以树状图列出目录的内容。 语 法:tree [-aACdDfFgilnNpqstux][-I <范本样式>][-P <范本样式 Map迭代方式,Map迭代,Map循环 蕃薯耀 Map循环Map迭代Map迭代方式 Map迭代方式,Map迭代,Map循环 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 蕃薯耀 2015年 Spring Cache注解+Redis hanqunfeng spring Spring3.1 Cache注解 依赖jar包: <!-- redis --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> Guava中针对集合的 filter和过滤功能 jackyrong filter 在guava库中,自带了过滤器(filter)的功能,可以用来对collection 进行过滤,先看例子: @Test public void whenFilterWithIterables_thenFiltered() { List<String> names = Lists.newArrayList("John" 学习编程那点事 lampcy 编程androidPHPhtml5 一年前的夏天,我还在纠结要不要改行,要不要去学php?能学到真本事吗?改行能成功吗?太多的问题,我终于不顾一切,下定决心,辞去了工作,来到传说中的帝都。老师给的乘车方式还算有效,很顺利的就到了学校,赶巧了,正好学校搬到了新校区。先安顿了下来,过了个轻松的周末,第一次到帝都,逛逛吧! 接下来的周一,是我噩梦的开始,学习内容对我这个零基础的人来说,除了勉强完成老师布置的作业外,我已经没有时间和精力去 架构师之流处理---------bytebuffer的mark,limit和flip nannan408 ByteBuffer 1.前言。 如题,limit其实就是可以读取的字节长度的意思,flip是清空的意思,mark是标记的意思 。 2.例子. 例子代码: String str = "helloWorld"; ByteBuffer buff = ByteBuffer.wrap(str.getBytes()); Sy org.apache.el.parser.ParseException: Encountered " ":" ": "" at line 1, column 1 Everyday都不同 $转义el表达式 最近在做Highcharts的过程中,在写js时,出现了以下异常: 严重: Servlet.service() for servlet jsp threw exception org.apache.el.parser.ParseException: Encountered " ":" ": "" at line 1, 用Java实现发送邮件到163 tntxia java实现 /* 在java版经常看到有人问如何用javamail发送邮件?如何接收邮件?如何访问多个文件夹等。问题零散,而历史的回复早已经淹没在问题的海洋之中。 本人之前所做过一个java项目,其中包含有WebMail功能,当初为用java实现而对javamail摸索了一段时间,总算有点收获。看到论坛中的经常有此方面的问题,因此把我的一些经验帖出来,希望对大家有些帮助。 此篇仅介绍用 探索实体类存在的真正意义 java小叶檀 POJO 一. 实体类简述 实体类其实就是俗称的POJO,这种类一般不实现特殊框架下的接口,在程序中仅作为数据容器用来持久化存储数据用的 POJO(Plain Old Java Objects)简单的Java对象 它的一般格式就是 public class A{ private String id; public Str 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。
严谨的小张觉得虽然当个普通互斥锁,已经稳稳够用,可是业务里总是又很多特殊情况的,比如A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题 。
而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态 。
小张不是杠精,因为库存操作总有这样那样的特殊。
所以我们希望在这种情况时,可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果,这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约。
读写分离也是常见,一个读多写少的业务为了性能,常常是有读锁和写锁的。
而此刻的扩展已经超出了一把简单轮子的复杂程度,光是处理续约,就够小张喝一壶,何况在性能(锁的最大等待时间)、优雅(无效锁申请)、重试(失败重试机制)等方面还要下功夫研究。
在小张苦思冥想时,旁边的小白凑过来看了看小张,很好奇,都2021年了,为什么不直接用redisson呢?
Redisson就有这把你要的锁。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 项目地址:https://github.com/YunaiV/yudao-cloud 视频教程:https://doc.iocoder.cn/video/
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
号称简单的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功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段。
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() 执行监听回调
runnableToExecute.run()
value.getLatch().release(); 释放信号量
value.getLatch().release();
Redisson通过LockPubSub 监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁。
这时再回来看tryAcquireOnceAsync另一分支
可以看到,无超时时间时,在执行加锁操作后,还执行了一段费解的逻辑
ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } }) } } }) 复制代码
此处涉及到Netty的Future/Promise-Listener模型,Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解
在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。
这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务。
这段定时任务的就是watchDog的核心。
2.锁续约
查看RedissonLock.this.scheduleExpirationRenewal(threadId)
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
internalLockLeaseTime / 3
renewExpirationAsync
• renewExpirationAsync重置了锁超时时间,又注册一个监听器,监听回调又执行了renewExpiration
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被唤醒开始继续抢锁,拿到锁
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方法。
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();
RLock fairLock = redissonClient.getFairLock(lockName);
fairLock.lock();
RedissonFairLock继承自RedissonLock,同样一路向下找到加锁实现方法tryLockInnerAsync 。
RedissonLock
这里有2段冗长的Lua,但是Debug发现,公平锁的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua较长,参数也多,我们着重分析Lua的实现规则
command == RedisCommands.EVAL_LONG
参数
-- 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个步骤 1.队列清理 保证队列中只有未过期的等待线程 2.首次加锁 hset加锁,pexpire过期时间 3.重入判断 此处同可重入锁lua 4.返回ttl 5.计算尾节点ttl 初始值为锁的剩余过期时间 6.末尾排队 ttl + 2 * currentTime + waitTime是score的默认值计算公式 2.模拟 如果模拟以下顺序,就会明了redisson公平锁整个加锁流程 假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20 t1:当线程1初次获取锁 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 && 不存在线程等待队列 成立 2.1 lpop和zerm、zincrby都是无效操作,只有加锁生效,说明是首次加锁,加锁后返回nil 加锁成功,线程1获取到锁,结束 t2:线程2尝试获取锁(线程1未释放锁) 1.等待队列无头节点,跳出死循环->2 2.不存在该锁 不成立->3 3.非重入线程 ->4 4.score无值 ->5 5.尾节点为空,设置ttl初始值为lock_name的ttl -> 6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列,线程2为头节点 score = 20S + 5000ms + 10:00:10 + 10:00:10 = 10:00:35 + 10:00:10 t3:线程3尝试获取锁(线程1未释放锁) 1.等待队列有头节点 1.1未过期->2 2.不存在该锁不成立->3 3.非重入线程->4 4.score无值 ->5 5.尾节点不为空 && 尾节点线程为2,非当前线程 5.1取出之前设置的score,减去当前时间:ttl = score - currentTime ->6 6.按照ttl + waitTime + currentTime + currentTime 来设置zSet超时时间score,并且加入等待队列 score = 10S + 5000ms + 10:00:20 + 10:00:20 = 10:00:35 + 10:00:20 如此一来,三个需要抢夺一把锁的线程,完成了一次排队,在list中排列他们等待线程id,在zSet中存放过期时间(便于排列优先级)。其中返回ttl的线程2客户端、线程3客户端将会一直按一定间隔自旋重复执行该段Lua,尝试加锁,如此一来便和AQS有了异曲同工之处。 而当线程1释放锁之后(这里依旧有通过Pub/Sub发布解锁消息,通知其他线程获取) 10:00:30 线程2尝试获取锁(线程1已释放锁) 1.等待队列有头节点,未过期->2 2.不存在该锁 & 等待队列头节点是当前线程 成立 2.1删除当前线程的队列信息和zSet信息,超时时间为: 线程2 10:00:35 + 10:00:10 - 10:00:30 = 10:00:15 线程3 10:00:35 + 10:00:20 - 10:00:30 = 10:00:25 2.2线程2获取到锁,重新设置过期时间 加锁成功,线程2获取到锁,结束 排队结构如图 公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。 由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。 并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。 六、总结 Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现。 Redisson也提供了多机情况下的联锁MultiLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock 和官方推荐的红锁RedLock: https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock 所以,当你真的需要分布式锁时,不妨先来Redisson里找找。 你可能感兴趣的:(java,redis,开发语言) 【开源】基于SSM框架“大学生艺术节”管理系统(计算机毕业设计)+万字毕业论文+远程部署+ppt+代码讲解 ssm284 计算机毕业设计_gzs 开源课程设计spring毕设mybatisjava毕业设计 系统合集跳转源码获取链接点击主页更能获取海量源码博主联系方式拉到下方点击名片获取!!!博主联系方式拉到下方点击名片获取!!!10年计算机开发经验,主营业务:源码获取、项目二开、语音辅导、远程调试、毕业设计、课程设计、毕业论文、BUG修改一、系统环境运行环境:最好是javajdk1.8,我们在这个平台上运行的。其他版本理论上也可以。IDE环境:Eclipse,Myeclipse,IDEA或者Spri 【数据序列化协议】Protocol Buffers 茉菇 网络 一、为什么需要序列化?数据跨平台/语言交互:不同编程语言(如Java、Python、Go)的数据结构不兼容,序列化提供统一的数据表示。例如:Java的HashMap和Python的dict需转换为通用格式(如JSON、Protobuf)才能通信。网络传输优化:原始内存中的对象包含指针、元数据等冗余信息,无法直接传输。序列化后数据体积更小,减少带宽占用,提升传输效率。持久化存储:将对象转换为字节流或 JavaScript 任务队列详解:Event Loop、宏任务与微任务 咖啡虫 前端中的一些概念及理解javascriptvim开发语言 JavaScript任务队列详解:EventLoop、宏任务与微任务在JavaScript的世界里,异步编程是一个至关重要的概念。JavaScript采用单线程运行方式,但能够处理异步任务,这一切都要归功于事件循环(EventLoop)机制。本文将深入剖析JavaScript的任务队列(TaskQueue),包括宏任务(Macrotask)和微任务(Microtask),并结合示例解析代码的执行顺 JavaScript函数-arguments的使用 難釋懷 javascript开发语言 在JavaScript编程语言中,函数是构建复杂逻辑和实现代码复用的关键组件。虽然现代JavaScript(尤其是ES6及之后版本)提供了更多灵活的方式来处理函数参数(如剩余参数、默认参数等),但arguments对象仍然是一个非常有用且强大的特性,尤其是在处理不定数量参数的场景中。本文将深入探讨arguments对象的使用方法及其应用场景。arguments对象简介在每个函数内部,都有一个名为a 前端 fetch API 调用 Tushare 的数据接口获取免费的基金股票信息数据 匹马夕阳 开发工具前端开源 要在前端使用JavaScript的fetchAPI调用Tushare的数据接口,您需要遵循以下步骤:1.注册Tushare账号并获取Token首先,访问Tushare官网注册账号。注册成功后,登录账号,在个人中心获取您的APIToken。2.构建请求参数Tushare的API接口采用POST请求方式,参数需要以JSON格式传递。以下是一个示例请求参数:{"api_name":"stock_basi linux下使用nohup命令不输出任何文件 Carrot_ly linux运维 nohup"command">/dev/null2>&1&"command"代表不使用nohup时的命令全部内容例:nohupjava-jaraircas-1.0.0.jar>/dev/null2>&1& Java Web开发 yourkin666 java前端开发语言 JavaWeb开发Java部分:面向对象后,注重学集合,抛异常,泛型,线程,反射,注解【Java零基础视频教程(适合Java基础,Java入门)老杜Java13版】https://www.bilibili.com/video/BV1mE411x7Wt?vd_source=4543341eea15096fa471f9067cc841ff【【零基础快速学Java】韩顺平零基础30天学会Java】htt AI赋能Spring Boot:打造智能应用的秘诀 墨瑾轩 一起学学Java【一】人工智能springboot后端 关注墨瑾轩,带你探索Java的奥秘超萌技术攻略,轻松晋级编程高手技术宝库已备好,就等你来挖掘订阅墨瑾轩,智趣学习不孤单即刻启航,编程之旅更有趣引言各位技术探险家们,欢迎来到今天的冒险——我们将一起探索如何将SpringBoot与AI服务集成,利用OpenAI和TensorFlow提升应用的智能。想象一下,你的应用程序不再是一个简单的代码集合,而是一个拥有智慧的伙伴,能够理解和预测用户的需求。这就是 Redis学习笔记——(17)Redis面试题及答案 码农小高 Redisredis学习数据库 Redis面试题1.什么是redis?Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sortedset--有序集合)和hash。这些数据结构都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内 Kotlin中Retrofit网络请求简单封装 weixin_34293059 移动开发java 使用Kotlin简单的对Retrofit+RxJava大多数普通请求进行封装提示:如果对Kotlin,RxLifecycle,Retrofit,RxJava等不太了解的小伙伴可以参考网上的基础资料1.惯例先添加依赖//Retrofitimplementation'com.squareup.retrofit2:retrofit:2.3.0'implementation'com.squareup.re 比Python、Java更快的 Go 语言,能否称霸江湖? weixin_33686714 pythonjavaruby 文章来源:jb51.net有一种语言堪称比语言排行榜前五热门选手的Python、Java更快,它就是GO语言。Go于2009年11月正式宣布推出,成为开放源代码项目,并在Linux及MacOSX平台上进行了实现,后来追加了Windows系统下的实现。在2016年,Go被软件评价公司TIOBE选为“TIOBE2016年最佳语言”。目前,Go每半年发布一个二级版本(即从a.x升级到a.y)。在Go语言 Java中的引用(reference)和C/C++中的指针 鱼力舟 HOT100javac语言c++ publicListNodedetectCycle(ListNodehead){HashSetlistNodes=newHashSet的作用HashSet用来存储链表中的节点(ListNode对象)。每个节点在链表中都有一个唯一的引用(即内存地址),而HashSet用来检查是否已经遍历过这个节点。在这段代码中,HashSet的add()方法返回false时,说明该节点已经存在于集合中,也就意味着我 前端数据类型终极指南:从JavaScript到TypeScript 念九_ysl typescript前端 前端数据类型全景解析.type-system{max-width:1000px;margin:0auto;padding:20px;}.type-card{border:1pxsolid#e0e0e0;padding:20px;margin:15px0;border-radius:8px;}.ts-badge{background:#3178c6;color:white;padding:2px5p Future和FutureTask实现类详解以及使用。 一个儒雅随和的男子 多线程java 前言Future是Java并发编程中的一个接口,用来表示异步计算的结果。它允许我们提交一个任务,然后之后再去获取结果,或者在结果可用时处理它。我们需要考虑Future的主要方法。根据文档,Future接口有几个关键方法:isDone()检查计算是否完成,get()获取结果(会阻塞直到完成),cancel()尝试取消任务,isCancelled()判断是否被取消。这些方法的作用和用法需要详细说明。然 Android 数据加载与分页业务 抹香鲸的弟弟蓝鲸 androidkotlinandroidjava 1,前言这篇文章写的是数据加载和分页。用到的库有'androidx.lifecycle:lifecycle-service:2.4.0''androidx.lifecycle:lifecycle-extensions:2.2.0''androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'io.reactivex.rxjava3:rxjava:3.0.4io 2.最多提取子串数目(100分)-附带Java逐行解析 最遥远的瞬间 华为OD机考试题算法 题目给定[a-z],26个英文字母小写字符串组成的字符串A和B,其中A可能存在重复字母,B不会存在重复字母,现从字符串A中按规则挑选一些字母,可以组成字符串B。挑选规则如下:同一个位置的字母只能挑选一次被挑选字母的相对先后顺序不能被改变求最多可以同时从A中挑选多少组能组成B的字符串。输入描述输入为2行,第1行输入字符串A,第2行输入字符串B,行首行尾没有多余空格,其中:A、B均由[a-z]26个英 蓝桥杯Java大学A组-15届省赛学习笔记 You_know_kho 蓝桥杯Java大学A组真题蓝桥杯java学习 蓝桥杯历届真题蓝桥杯历届真题蓝桥杯评测平台蓝桥杯模拟考试结果填空题试题A:拼正方形解题思路题目中数据很大,这并不重要,找到解法后100和100万并无本质区别。首先,如果只有一种2x2规格的正方形,那么平方数个小正方形正好可以拼出一个大正方形,例如4个拼出4x4,9个拼出6x6。其次,四个1x1规格的正方形可以拼出一个2x2规格的正方形。结合以上两点,先计算1x1规格的正方形可以拼出多少个2x2规格 DeepSeek写俄罗斯方块手机小游戏 浅痕~ 大模型游戏DeepSeek手机小游戏 DeepSeek写俄罗斯方块手机小游戏提问根据提的要求,让DeepSeek整理的需求,进行提问,内容如下:请生成一个包含以下功能的可运行移动端俄罗斯方块H5文件:核心功能要求原生JavaScript实现,适配手机屏幕支持触摸操作:左右滑动移动、点击屏幕旋转方块、往下滑方块快速下落下方显示虚拟控制按钮(左移/旋转/右移)、暂停/继续、重新开始按钮经典下落机制(自动下落、碰撞检测、行消除)右上角全屏切 Redis数据库面试——数据结构类型知识 Good Note 补档数据库redis面试服务端后端数据结构缓存 大家好,这里是GoodNote,关注公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Redis提供的5种基本数据结构类型和4种特殊类型,除此之外,还有8种底层数据结构,每种结构类型有其特点和适用场景。文章目录基本数据类型1.String(字符串)使用场景缓存计数器ID生成器分布式锁2.Hash(哈希)3.List(链表/列表)4.Set(集合)5.SortedSet(有序集合)特殊 Hutool - Http:基于 HttpUrlConnection 的 Http 客户端封装 五行星辰 业务系统应用技术hive大数据hadoopjava后端 一、简介在现代的软件开发中,与外部服务进行HTTP通信是非常常见的需求,比如调用第三方API、获取网页内容等。Java标准库中的HttpUrlConnection提供了基本的HTTP请求功能,但使用起来较为繁琐,需要处理很多细节,如连接管理、请求头设置、响应处理等。Hutool-Http模块对HttpUrlConnection进行了封装,提供了简洁易用的API,使得开发者可以更方便地进行HTTP请 Yarn 依赖解析机制深度解析:确定性安装与版本冲突的工程哲学 引言:包管理工具的"圣杯问题"在现代前端工程中,依赖管理已成为构建稳定性的核心挑战。根据2023年JavaScript生态调查报告显示,平均每个前端项目依赖1,200+个第三方包,嵌套依赖层级超过15层。在这样的复杂度下,如何实现确定性安装(DeterministicInstallation)和版本冲突智能解决,成为Yarn这类包管理工具的核心战场。本文将深入探讨Yarn(特别是Classicv1 Spring Bean 如何保证并发安全??? G丶AEOM 八股普通学习区java八股spring SpringBean如何保证并发安全简单来说:1、可以设置Beon的作用域为原型,这样每次从容器中获取该Bean时,都会创建一个新的实例,避免了多线程共享同一个对象实例的问题2、在不改变Beon的作用域的情况下,可以避免在Beon中存在可变状态的声明,尽量将状态信息存在方法内部的局部变量中,或者使用线程安全的数据结构,如ConcurrentHashMap来管理状态3、使用Java并发编程中提供的锁 leetcode 205. 同构字符串-java实现 依嘫_吃代码 LeetCodeleetcodejava算法 题目所属分类华为校招原题链接给定两个字符串s和t,判断它们是否是同构的。如果s中的字符可以按某种映射关系替换得到t,那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。代码案例:输入:s=“egg”,t=“add”输出:true输入:s=“foo”,t=“bar”输出:fals ssm神马物流+vue 源码哆哆V+ymhydo Java毕设优质源码vue.js前端javascript 摘要本神马物流管理系统设计目标是实现神马物流的信息化管理,提高管理效率,使得神马物流管理作规范化、科学化、高效化。本文重点阐述了神马物流管理系统的开发过程,以实际运用为开发背景,基于SSM+Vue框架,运用了Java编程语言和MYSQL数据库进行开发,充分保证系统的安全性和稳定性。本系统界面良好,操作简单方便,通过系统概述、系统分析、系统设计、数据库设计、系统测试这几个部分,详细的说明了系统的开发 2025蓝桥杯JAVA编程题练习Day4 起床悠悠 Java蓝桥杯算法学习蓝桥杯职场和发展java算法 1.艺术与篮球问题描述小蓝出生在一个艺术与运动并重的家庭中。妈妈是位书法家,她希望小蓝能通过练习书法,继承她的艺术天赋,并练就一手好字。爸爸是一名篮球教练,他希望小蓝能通过篮球锻炼身体,培养运动的激情和团队合作的精神。为了既满足妈妈的期望,又不辜负爸爸的心意,小蓝决定根据日期的笔画数来安排自己的练习。首先,他会将当天的日期按照“YYYYMMDD”的格式转换成一个8位数,然后将这8位数对应到汉字上, 【华为OD技术面试手撕真题】98、同构字符串 | 手撕真题+思路参考+代码解析(C & C++ & Java & Python & JS)(0ms) KJ.JK 华为OD技术面试手撕真题华为od面试c语言华为od机试真题华为od机试E卷javascript同构字符串 文章目录一、题目题目描述样例1二、代码参考C语言思路C语言代码C++语言思路C++代码Java语言思路Java代码Python语言思路Python代码JS语言思路JS代码作者:KJ.JK个人博客首页:KJ.JK专栏介绍:本专栏更新每年华为OD机试的高频手撕代码题,每个题目都会使用五种语言进行解答(C&C++&Java&Python&JS),思路分析都非常详细,争取实现最低的时间复杂度和高通过率,每 2025蓝桥杯JAVA编程题练习Day5 起床悠悠 蓝桥杯Java算法学习算法数据结构java蓝桥杯 1.最少步数【省模拟赛】问题描述小蓝要上一个楼梯,楼梯共有n级台阶(即小蓝总共要走nn级)。小蓝每一步可以走1级、2级或3级台阶。请问小蓝至少要多少步才能上到楼梯顶端?输入格式输入一行包含一个整数n。输出格式输出一行包含一个整数,表示答案。样例输入19样例输出13样例输入210样例输出24AC代码importjava.util.*;publicclassexercise1{staticScanne redis sentinel配置“sentinel parallel-syncs mymaster 1“详解 学会了没 redissentinelbootstrap 在RedisSentinel的配置中,sentinelparallel-syncsmymaster1这一行配置的作用是控制故障转移(Failover)后,允许同时向新主节点(NewMaster)发起数据同步的从节点(Slave)数量。下面通过场景示例详细解释:参数定义配置格式:sentinelparallel-syncs:监控的主节点名称(如mymaster)。:允许同时同步的从节点数量。默认值: Redis 常用的五种数据类型 HinINAX redischrome数据库 Redis常用的五种数据类型一、字符串(String)简介字符串是Redis中最基本的数据类型,可以存储任何类型的数据(如文本、数字、二进制数据等)。每个字符串的最大长度可以达到512MB。常用命令SETkeyvalue用途:设置键key的值为value。语法:SETkeyvalue示例:SETusername"john_doe"GETkey用途:获取键key的值。语法:GETkey示例:GETu java后端开发day19--学生管理系统升级 元亓亓亓 java后端开发java开发语言 (以下内容全部来自上述课程)1.要求及思路1.总体框架2.注册3.登录4.忘记密码2.代码1.javabeanpublicclassUser1{privateStringusername;privateStringpassword;privateStringpersonID;privateStringphoneNumber;publicUser1(){}publicUser1(Stringuser ztree设置禁用节点 3213213333332132 JavaScriptztreejsonsetDisabledNodeAjax ztree设置禁用节点的时候注意,当使用ajax后台请求数据,必须要设置为同步获取数据,否者会获取不到节点对象,导致设置禁用没有效果。 $(function(){ showTree(); setDisabledNode(); }); JVM patch by Taobao bookjovi javaHotSpot 在网上无意中看到淘宝提交的hotspot patch,共四个,有意思,记录一下。 7050685:jsdbproc64.sh has a typo in the package name 7058036:FieldsAllocationStyle=2 does not work in 32-bit VM 7060619:C1 should respect inline and 将session存储到数据库中 dcj3sjt126com sqlPHPsession CREATE TABLE sessions ( id CHAR(32) NOT NULL, data TEXT, last_accessed TIMESTAMP NOT NULL, PRIMARY KEY (id) ); <?php /** * Created by PhpStorm. * User: michaeldu * Date Vector 171815164 vector public Vector<CartProduct> delCart(Vector<CartProduct> cart, String id) { for (int i = 0; i < cart.size(); i++) { if (cart.get(i).getId().equals(id)) { cart.remove(i); 各连接池配置参数比较 g21121 连接池 排版真心费劲,大家凑合看下吧,见谅~ Druid DBCP C3P0 Proxool 数据库用户名称 Username Username User 数据库密码 Password Password Password 驱动名 [简单]mybatis insert语句添加动态字段 53873039oycg mybatis mysql数据库,id自增,配置如下: <insert id="saveTestTb" useGeneratedKeys="true" keyProperty="id" parameterType=& struts2拦截器配置 云端月影 struts2拦截器 struts2拦截器interceptor的三种配置方法 方法1. 普通配置法 <struts> <package name="struts2" extends="struts-default"> & IE中页面不居中,火狐谷歌等正常 aijuans IE中页面不居中 问题是首页在火狐、谷歌、所有IE中正常显示,列表页的页面在火狐谷歌中正常,在IE6、7、8中都不中,觉得可能那个地方设置的让IE系列都不认识,仔细查看后发现,列表页中没写HTML模板部分没有添加DTD定义,就是<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3 String,int,Integer,char 几个类型常见转换 antonyup_2006 htmlsql.net 如何将字串 String 转换成整数 int? int i = Integer.valueOf(my_str).intValue(); int i=Integer.parseInt(str); 如何将字串 String 转换成Integer ? Integer integer=Integer.valueOf(str); 如何将整数 int 转换成字串 String ? 1. PL/SQL的游标类型 百合不是茶 显示游标(静态游标)隐式游标游标的更新和删除%rowtyperef游标(动态游标) 游标是oracle中的一个结果集,用于存放查询的结果; PL/SQL中游标的声明; 1,声明游标 2,打开游标(默认是关闭的); 3,提取数据 4,关闭游标 注意的要点:游标必须声明在declare中,使用open打开游标,fetch取游标中的数据,close关闭游标 隐式游标:主要是对DML数据的操作隐 JUnit4中@AfterClass @BeforeClass @after @before的区别对比 bijian1013 JUnit4单元测试 一.基础知识 JUnit4使用Java5中的注解(annotation),以下是JUnit4常用的几个annotation: @Before:初始化方法 对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)@After:释放资源 对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次 精通Oracle10编程SQL(12)开发包 bijian1013 oracle数据库plsql /* *开发包 *包用于逻辑组合相关的PL/SQL类型(例如TABLE类型和RECORD类型)、PL/SQL项(例如游标和游标变量)和PL/SQL子程序(例如过程和函数) */ --包用于逻辑组合相关的PL/SQL类型、项和子程序,它由包规范和包体两部分组成 --建立包规范:包规范实际是包与应用程序之间的接口,它用于定义包的公用组件,包括常量、变量、游标、过程和函数等 --在包规 【EhCache二】ehcache.xml配置详解 bit1129 ehcache.xml 在ehcache官网上找了多次,终于找到ehcache.xml配置元素和属性的含义说明文档了,这个文档包含在ehcache.xml的注释中! ehcache.xml : http://ehcache.org/ehcache.xml ehcache.xsd : http://ehcache.org/ehcache.xsd ehcache配置文件的根元素是ehcahe ehcac java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderL 白糖_ javaeclipsespringtomcatWeb 今天学习spring+cxf的时候遇到一个问题:在web.xml中配置了spring的上下文监听器: <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> 随后启动 angular.element boyitech AngularJSAngularJS APIangular.element angular.element 描述: 包裹着一部分DOM element或者是HTML字符串,把它作为一个jQuery元素来处理。(类似于jQuery的选择器啦) 如果jQuery被引入了,则angular.element就可以看作是jQuery选择器,选择的对象可以使用jQuery的函数;如果jQuery不可用,angular.e java-给定两个已排序序列,找出共同的元素。 bylijinnan java import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class CommonItemInTwoSortedArray { /** * 题目:给定两个已排序序列,找出共同的元素。 * 1.定义两个指针分别指向序列的开始。 * 如果指向的两个元素 sftp 异常,有遇到的吗?求解 Chen.H javajcraftauthjschjschexception com.jcraft.jsch.JSchException: Auth cancel at com.jcraft.jsch.Session.connect(Session.java:460) at com.jcraft.jsch.Session.connect(Session.java:154) at cn.vivame.util.ftp.SftpServerAccess.connec [生物智能与人工智能]神经元中的电化学结构代表什么? comsci 人工智能 我这里做一个大胆的猜想,生物神经网络中的神经元中包含着一些化学和类似电路的结构,这些结构通常用来扮演类似我们在拓扑分析系统中的节点嵌入方程一样,使得我们的神经网络产生智能判断的能力,而这些嵌入到节点中的方程同时也扮演着"经验"的角色.... 我们可以尝试一下...在某些神经 通过LAC和CID获取经纬度信息 dai_lm laccid 方法1: 用浏览器打开http://www.minigps.net/cellsearch.html,然后输入lac和cid信息(mcc和mnc可以填0),如果数据正确就可以获得相应的经纬度 方法2: 发送HTTP请求到http://www.open-electronics.org/celltrack/cell.php?hex=0&lac=<lac>&cid=& JAVA的困难分析 datamachine java 前段时间转了一篇SQL的文章(http://datamachine.iteye.com/blog/1971896),文章不复杂,但思想深刻,就顺便思考了一下java的不足,当砖头丢出来,希望引点和田玉。 ----------------------------------------------------------------------------------------- 小学5年级英语单词背诵第二课 dcj3sjt126com englishword money 钱 paper 纸 speak 讲,说 tell 告诉 remember 记得,想起 knock 敲,击,打 question 问题 number 数字,号码 learn 学会,学习 street 街道 carry 搬运,携带 send 发送,邮寄,发射 must 必须 light 灯,光线,轻的 front linux下面没有tree命令 dcj3sjt126com linux centos p安装 yum -y install tree mac os安装 brew install tree 首先来看tree的用法 tree 中文解释:tree 功能说明:以树状图列出目录的内容。 语 法:tree [-aACdDfFgilnNpqstux][-I <范本样式>][-P <范本样式 Map迭代方式,Map迭代,Map循环 蕃薯耀 Map循环Map迭代Map迭代方式 Map迭代方式,Map迭代,Map循环 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 蕃薯耀 2015年 Spring Cache注解+Redis hanqunfeng spring Spring3.1 Cache注解 依赖jar包: <!-- redis --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> Guava中针对集合的 filter和过滤功能 jackyrong filter 在guava库中,自带了过滤器(filter)的功能,可以用来对collection 进行过滤,先看例子: @Test public void whenFilterWithIterables_thenFiltered() { List<String> names = Lists.newArrayList("John" 学习编程那点事 lampcy 编程androidPHPhtml5 一年前的夏天,我还在纠结要不要改行,要不要去学php?能学到真本事吗?改行能成功吗?太多的问题,我终于不顾一切,下定决心,辞去了工作,来到传说中的帝都。老师给的乘车方式还算有效,很顺利的就到了学校,赶巧了,正好学校搬到了新校区。先安顿了下来,过了个轻松的周末,第一次到帝都,逛逛吧! 接下来的周一,是我噩梦的开始,学习内容对我这个零基础的人来说,除了勉强完成老师布置的作业外,我已经没有时间和精力去 架构师之流处理---------bytebuffer的mark,limit和flip nannan408 ByteBuffer 1.前言。 如题,limit其实就是可以读取的字节长度的意思,flip是清空的意思,mark是标记的意思 。 2.例子. 例子代码: String str = "helloWorld"; ByteBuffer buff = ByteBuffer.wrap(str.getBytes()); Sy org.apache.el.parser.ParseException: Encountered " ":" ": "" at line 1, column 1 Everyday都不同 $转义el表达式 最近在做Highcharts的过程中,在写js时,出现了以下异常: 严重: Servlet.service() for servlet jsp threw exception org.apache.el.parser.ParseException: Encountered " ":" ": "" at line 1, 用Java实现发送邮件到163 tntxia java实现 /* 在java版经常看到有人问如何用javamail发送邮件?如何接收邮件?如何访问多个文件夹等。问题零散,而历史的回复早已经淹没在问题的海洋之中。 本人之前所做过一个java项目,其中包含有WebMail功能,当初为用java实现而对javamail摸索了一段时间,总算有点收获。看到论坛中的经常有此方面的问题,因此把我的一些经验帖出来,希望对大家有些帮助。 此篇仅介绍用 探索实体类存在的真正意义 java小叶檀 POJO 一. 实体类简述 实体类其实就是俗称的POJO,这种类一般不实现特殊框架下的接口,在程序中仅作为数据容器用来持久化存储数据用的 POJO(Plain Old Java Objects)简单的Java对象 它的一般格式就是 public class A{ private String id; public Str 按字母分类: 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个步骤
redisson_lock_queue:{xxx}
redisson_lock_timeout:{xxx}
1.队列清理
保证队列中只有未过期的等待线程
2.首次加锁
hset加锁,pexpire过期时间
3.重入判断
此处同可重入锁lua
4.返回ttl
5.计算尾节点ttl
初始值为锁的剩余过期时间
6.末尾排队
ttl + 2 * currentTime + waitTime是score的默认值计算公式
ttl + 2 * currentTime + waitTime
2.模拟
如果模拟以下顺序,就会明了redisson公平锁整个加锁流程
假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20
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:
https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock
和官方推荐的红锁RedLock:
https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#84-红锁redlock
所以,当你真的需要分布式锁时,不妨先来Redisson里找找。