Spring RedisCache 死锁解决方案

Spring RedisCache 偶发死锁

  • 场景说明
    • spring缓存注解用到的最终核心类结构
    • spring方法调用流程图
    • 总结

场景说明

spring框架使用redis缓存,使用@cacheable,@cacheput注解
因为缓存击穿导致数据库压力过大,因此我们通过加锁的方式解决,然而spring4.3版本之后增加了cacheable注解增加了sync属性,所以直接用吧。cacheput注解没有sync属性,按理说他是不需要锁的,但是Spring redis框架里面执行到最后又加锁了。最后调试一不小心就死锁了。

spring缓存注解用到的最终核心类结构

Spring RedisCache 死锁解决方案_第1张图片
按照我们的需求是在@cacheable注解上加锁,spring框架如是这么定义。

spring方法调用流程图

Spring RedisCache 死锁解决方案_第2张图片

AbstractRedisCacheCallback

static abstract class AbstractRedisCacheCallback<T> implements RedisCallback<T> {

		private long WAIT_FOR_LOCK_TIMEOUT = 300;
		private final BinaryRedisCacheElement element;
		private final RedisCacheMetadata cacheMetadata;

		public AbstractRedisCacheCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
			this.element = element;
			this.cacheMetadata = metadata;
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.data.redis.core.RedisCallback#doInRedis(org.springframework.data.redis.connection.RedisConnection)
		 */
		@Override
		public T doInRedis(RedisConnection connection) throws DataAccessException {
			//这里的代码是导致问题的所在的根本原因=====
			//waitForLock(connection);//这行注释
			return doInRedis(element, connection);
		}

	    //
		public abstract T doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException;

		protected void processKeyExpiration(RedisCacheElement element, RedisConnection connection) {
			if (!element.isEternal()) {
				connection.expire(element.getKeyBytes(), element.getTimeToLive());
			}
		}

		protected void maintainKnownKeys(RedisCacheElement element, RedisConnection connection) {

			if (!element.hasKeyPrefix()) {

				connection.zAdd(cacheMetadata.getSetOfKnownKeysKey(), 0, element.getKeyBytes());

				if (!element.isEternal()) {
					connection.expire(cacheMetadata.getSetOfKnownKeysKey(), element.getTimeToLive());
				}
			}
		}

		protected void cleanKnownKeys(RedisCacheElement element, RedisConnection connection) {

			if (!element.hasKeyPrefix()) {
				connection.zRem(cacheMetadata.getSetOfKnownKeysKey(), element.getKeyBytes());
			}
		}

		protected boolean waitForLock(RedisConnection connection) {

			boolean retry;
			boolean foundLock = false;
			do {
				retry = false;
				if (connection.exists(cacheMetadata.getCacheLockKey())) {
					foundLock = true;
					try {
						Thread.sleep(WAIT_FOR_LOCK_TIMEOUT);
					} catch (InterruptedException ex) {
						Thread.currentThread().interrupt();
					}
					retry = true;
				}
			} while (retry);

			return foundLock;
		}

		protected void lock(RedisConnection connection) {
		    //这里增加可重入锁,使获取缓存时间到期,再次进入拦截二次获取缓存
		    if(ThreadLocalUtil.constainskey(){
		       			waitForLock(connection);
			           connection.set(cacheMetadata.getCacheLockKey(), "locked".getBytes());
		    }

		}

		protected void unlock(RedisConnection connection) {
		    //redis删除不存在的key不会报错
			connection.del(cacheMetadata.getCacheLockKey());
		}
	}

RedisWriteThroughCallback

static class RedisWriteThroughCallback extends AbstractRedisCacheCallback<byte[]> {

		public RedisWriteThroughCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
			super(element, metadata);
		}

		@Override
		public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {

			try {
				lock(connection);
				try {

					byte[] value = connection.get(element.getKeyBytes());

					if (value != null) {
						return value;
					}

					if (!isClusterConnection(connection)) {

						connection.watch(element.getKeyBytes());
						connection.multi();
					}

					value = element.get();

					if (value.length == 0) {
						connection.del(element.getKeyBytes());
					} else {
						connection.set(element.getKeyBytes(), value);
						processKeyExpiration(element, connection);
						maintainKnownKeys(element, connection);
					}

					if (!isClusterConnection(connection)) {
						connection.exec();
					}

					return value;
				} catch (RuntimeException e) {
					if (!isClusterConnection(connection)) {
						connection.discard();
					}
					throw e;
				}
			} finally {
				unlock(connection);
			}
		}
	};

注意上面的中文注释代码。

总结

  1. 如果遇到死锁,先删除,关键字是:缓存名字+~lock
  2. 解决了@cacheput注解等待锁的问题。
  3. 解决了@Cacheable注解可重入锁的问题
  4. 当然也是不存在死锁的问题了。

你可能感兴趣的:(rediscache)