5959import org .springframework .data .redis .listener .PatternTopic ;
6060import org .springframework .data .redis .listener .RedisMessageListenerContainer ;
6161import org .springframework .data .redis .listener .Topic ;
62- import org .springframework .integration .support .locks .DefaultLockRegistry ;
6362import org .springframework .integration .support .locks .DistributedLock ;
6463import org .springframework .integration .support .locks .ExpirableLockRegistry ;
6564import org .springframework .integration .support .locks .RenewableLockRegistry ;
7978 * Locks are reentrant.
8079 * <p>
8180 * <b>However, locks are scoped by the registry; a lock from a different registry with the
82- * same key (even if the registry uses the same 'registryKey') are different
81+ * same key (even if the registry uses the same 'registryKey') is different
8382 * locks, and the second cannot be acquired by the same thread while the first is
8483 * locked.</b>
8584 * <p>
86- * <b>Note: This is not intended for low latency applications.</b> It is intended
85+ * <b>Note: This is not intended for low- latency applications.</b> It is intended
8786 * for resource locking across multiple JVMs.
8887 * <p>
8988 * {@link Condition}s are not supported.
89+ * <p>
90+ * The lock instances are cached by their key for future reuse.
91+ * If all the locks in the cache are acquired,
92+ * the {@link #obtain(Object)} method throws {@link CannotAcquireLockException}.
93+ * Otherwise, the old, unused locks are evicted from the cache.
94+ * An attempt to lock such an orphaned {@link RedisLock} throws another {@link CannotAcquireLockException}
95+ * to avoid double locking from different threads on the same key.
9096 *
9197 * @author Gary Russell
9298 * @author Konstantin Yakimov
@@ -114,16 +120,14 @@ public final class RedisLockRegistry
114120
115121 private static final long DEFAULT_EXPIRE_AFTER = 60000L ;
116122
117- private static final int DEFAULT_CAPACITY = 256 ;
123+ private static final int DEFAULT_CAPACITY = 100_000 ;
118124
119125 private static final int DEFAULT_IDLE = 100 ;
120126
121127 private final Lock lock = new ReentrantLock ();
122128
123129 private Duration idleBetweenTries = Duration .ofMillis (DEFAULT_IDLE );
124130
125- private DefaultLockRegistry defaultLockRegistry = new DefaultLockRegistry ();
126-
127131 private final Map <String , RedisLock > locks =
128132 new LinkedHashMap <>(16 , 0.75F , true ) {
129133
@@ -132,7 +136,8 @@ public final class RedisLockRegistry
132136
133137 @ Override
134138 protected boolean removeEldestEntry (Entry <String , RedisLock > eldest ) {
135- return size () > RedisLockRegistry .this .cacheCapacity ;
139+ return size () > RedisLockRegistry .this .cacheCapacity
140+ && !eldest .getValue ().isAcquiredInThisProcess ();
136141 }
137142
138143 };
@@ -249,14 +254,18 @@ public void setRenewalTaskScheduler(TaskScheduler renewalTaskScheduler) {
249254
250255 /**
251256 * Set the capacity of cached locks.
252- * @param cacheCapacity The capacity of cached lock, (default 256 locks).
257+ * If the number of currently cached locks exceeds the new capacity,
258+ * this method tries to evict unused locks.
259+ * @param cacheCapacity The capacity of cached lock, (default 100_000).
253260 * @since 5.5.6
261+ * @see #expireUnusedOlderThan(long)
254262 */
255263 public void setCacheCapacity (int cacheCapacity ) {
264+ Assert .isTrue (cacheCapacity > 0 , "'cacheCapacity' must be greater than 0" );
265+ if (this .locks .size () > cacheCapacity ) {
266+ expireUnusedOlderThan (0 );
267+ }
256268 this .cacheCapacity = cacheCapacity ;
257- // Find the highest power of 2 for (n + 1), then subtract 1 to get the mask
258- int mask = Integer .highestOneBit (cacheCapacity + 1 ) - 1 ;
259- this .defaultLockRegistry = new DefaultLockRegistry (mask );
260269 }
261270
262271 /**
@@ -290,7 +299,22 @@ public DistributedLock obtain(Object lockKey) {
290299 String path = (String ) lockKey ;
291300 this .lock .lock ();
292301 try {
293- return this .locks .computeIfAbsent (path , getRedisLockConstructor (this .redisLockType ));
302+ RedisLock redisLock = this .locks .computeIfAbsent (path , getRedisLockConstructor (this .redisLockType ));
303+ if (!redisLock .isAcquiredInThisProcess () && this .locks .size () > this .cacheCapacity ) {
304+ this .locks .entrySet ()
305+ .removeIf (entry -> {
306+ RedisLock lock = entry .getValue ();
307+ return lock != redisLock && !lock .isAcquiredInThisProcess ();
308+ });
309+
310+ if (this .locks .size () > this .cacheCapacity ) {
311+ this .locks .remove (path );
312+ throw new CannotAcquireLockException ("There are already " +
313+ this .cacheCapacity + " unique Redis locks acquired in the application " +
314+ "from " + this + ". Cannot obtain more at the moment." );
315+ }
316+ }
317+ return redisLock ;
294318 }
295319 finally {
296320 this .lock .unlock ();
@@ -305,11 +329,7 @@ public void expireUnusedOlderThan(long age) {
305329 this .locks .entrySet ()
306330 .removeIf (entry -> {
307331 RedisLock lock = entry .getValue ();
308- long lockedAt = lock .getLockedAt ();
309- return now - lockedAt > age
310- // 'lockedAt = 0' means that the lock is still not acquired!
311- && lockedAt > 0
312- && !lock .isAcquiredInThisProcess ();
332+ return now - lock .getLockedAt () >= age && !lock .isAcquiredInThisProcess ();
313333 });
314334 }
315335 finally {
@@ -359,6 +379,13 @@ public void renewLock(Object lockKey, Duration ttl) {
359379 }
360380 }
361381
382+ @ Override
383+ public String toString () {
384+ return "RedisLockRegistry{" +
385+ "registryKey=" + this .registryKey +
386+ '}' ;
387+ }
388+
362389 /**
363390 * The mode in which this registry is going to work with locks.
364391 */
@@ -410,19 +437,19 @@ private abstract class RedisLock implements DistributedLock {
410437 public static final RedisScript <Boolean > RENEW_REDIS_SCRIPT =
411438 new DefaultRedisScript <>(RENEW_SCRIPT , Boolean .class );
412439
413- protected final String lockKey ;
440+ private final String key ;
414441
415- private final ReentrantLock localLock ;
442+ protected final String lockKey ;
416443
417- private int holdCount ;
444+ private final ReentrantLock localLock = new ReentrantLock () ;
418445
419- private volatile long lockedAt ;
446+ private volatile long lockedAt = System . currentTimeMillis () ;
420447
421448 private volatile @ Nullable ScheduledFuture <?> renewFuture ;
422449
423450 private RedisLock (String path ) {
451+ this .key = path ;
424452 this .lockKey = constructLockKey (path );
425- this .localLock = (ReentrantLock ) RedisLockRegistry .this .defaultLockRegistry .obtain (this .lockKey );
426453 }
427454
428455 private String constructLockKey (String path ) {
@@ -460,7 +487,6 @@ public void lock(Duration ttl) {
460487 while (true ) {
461488 try {
462489 if (tryRedisLock (-1L , ttl .toMillis ())) {
463- this .holdCount ++;
464490 return ;
465491 }
466492 }
@@ -488,7 +514,6 @@ public final void lockInterruptibly() throws InterruptedException {
488514 while (true ) {
489515 try {
490516 if (tryRedisLock (-1L , RedisLockRegistry .this .expireAfter .toMillis ())) {
491- this .holdCount ++;
492517 return ;
493518 }
494519 }
@@ -530,9 +555,6 @@ public boolean tryLock(Duration waitTime, Duration ttl) throws InterruptedExcept
530555 if (!acquired ) {
531556 this .localLock .unlock ();
532557 }
533- else {
534- this .holdCount ++;
535- }
536558 return acquired ;
537559 }
538560 catch (Exception e ) {
@@ -543,6 +565,10 @@ public boolean tryLock(Duration waitTime, Duration ttl) throws InterruptedExcept
543565 }
544566
545567 private boolean tryRedisLock (long time , long expireAfter ) throws ExecutionException , InterruptedException {
568+ if (!RedisLockRegistry .this .locks .containsKey (this .key )) {
569+ throw new IllegalStateException ("The lock '" + this .key + "' was evicted from the exhausted cache. " +
570+ "Obtain a fresh one from " + RedisLockRegistry .this );
571+ }
546572 final boolean acquired = tryRedisLockInner (time , expireAfter );
547573 if (acquired ) {
548574 if (LOGGER .isDebugEnabled ()) {
@@ -571,8 +597,7 @@ public final void unlock() {
571597 if (!this .localLock .isHeldByCurrentThread ()) {
572598 throw new IllegalStateException ("You do not own lock at " + this .lockKey );
573599 }
574- if (this .holdCount > 1 ) {
575- this .holdCount --;
600+ if (this .localLock .getHoldCount () > 1 ) {
576601 this .localLock .unlock ();
577602 return ;
578603 }
@@ -617,7 +642,6 @@ public final void unlock() {
617642 ReflectionUtils .rethrowRuntimeException (e );
618643 }
619644 finally {
620- this .holdCount --;
621645 this .localLock .unlock ();
622646 }
623647 }
@@ -667,7 +691,7 @@ public String toString() {
667691 SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd@HH:mm:ss.SSS" );
668692 return "RedisLock [lockKey=" + this .lockKey
669693 + ",lockedAt=" + dateFormat .format (new Date (this .lockedAt ))
670- + ", clientId=" + RedisLockRegistry .this . clientId
694+ + ", clientId=" + RedisLockRegistry .this
671695 + "]" ;
672696 }
673697
@@ -676,7 +700,7 @@ public int hashCode() {
676700 final int prime = 31 ;
677701 int result = 1 ;
678702 result = prime * result + getOuterType ().hashCode ();
679- result = prime * result + (( this .lockKey == null ) ? 0 : this . lockKey . hashCode () );
703+ result = prime * result + this .lockKey . hashCode ();
680704 result = prime * result + Long .hashCode (this .lockedAt );
681705 result = prime * result + RedisLockRegistry .this .clientId .hashCode ();
682706 return result ;
@@ -687,9 +711,6 @@ public boolean equals(Object obj) {
687711 if (this == obj ) {
688712 return true ;
689713 }
690- if (obj == null ) {
691- return false ;
692- }
693714 if (getClass () != obj .getClass ()) {
694715 return false ;
695716 }
@@ -763,7 +784,7 @@ private boolean subscribeLock(long time, long expireAfter) throws ExecutionExcep
763784 return true ;
764785 }
765786 try {
766- //if short expireAfter key expire for ttl, no receive unlock msg
787+ //if short expireAfter key expire for ttl, no received unlock msg
767788 long waitTime = time >= 0 ? time : RedisLockRegistry .this .expireAfter .toMillis ();
768789 future .get (waitTime , TimeUnit .MILLISECONDS );
769790 }
0 commit comments