Springboot 2.0.x 結合Redis 分佈式鎖

1. 引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.properties增加以下配置

## redis配置
spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=slo
2.加入配置類
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * <p>redis緩存配置</p>
 */
@Configuration
@EnableCaching
public class RedisCachedConfig extends CachingConfigurerSupport {
	//過期時間1天
//	private Duration timeToLive = Duration.ofMinutes(1);
	private Duration timeToLive = Duration.ofHours(1);
	@Bean
	public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
		//默認1
		RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(this.timeToLive)
				.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
				.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
				.disableCachingNullValues();
		RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
				.cacheDefaults(config)
				.transactionAware()
				.build();
		return redisCacheManager;
	}
	@Bean(name = "redisTemplate")
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
		redisTemplate.setConnectionFactory(redisConnectionFactory);
		redisTemplate.setKeySerializer(keySerializer());
		redisTemplate.setHashKeySerializer(keySerializer());
		redisTemplate.setValueSerializer(valueSerializer());
		redisTemplate.setHashValueSerializer(valueSerializer());
		return redisTemplate;
	}
	private RedisSerializer<String> keySerializer() {
		return new StringRedisSerializer();
	}
	private RedisSerializer<Object> valueSerializer() {
		return new GenericJackson2JsonRedisSerializer();
	}
}

3.編寫Lock工具類

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

import org.springframework.data.redis.core.RedisTemplate;

import lombok.Data;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Data
@Component
public class RedisLock {
	private final Logger logger = LoggerFactory.getLogger(this.getClass());

	@Resource
	private RedisTemplate<String, Object> redisTemplate;
	/**
	 * 重試時間
	 */
	private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 100;
	/**
	 * 鎖的後綴
	 */
	private static final String LOCK_SUFFIX = "_redis_lock";
	/**
	 * 鎖的key
	 */
	private String lockKey;

	/**
	 * 鎖超時時間,防止線程在入鎖以後,防止阻塞後面的線程無法獲取鎖
	 */
	private int expireMsecs = 2 * 60 * 1000;
	/**
	 * 線程獲取鎖的等待時間
	 */
	private int timeoutMsecs = 10 * 1000;
	/**
	 * 是否鎖定標誌
	 */
	private volatile boolean locked = false;


	/**
	 * 封裝和jedis方法
	 *
	 * @param key
	 * @return
	 */
	private String get(final String key) {
		Object obj = redisTemplate.opsForValue().get(key);
		return obj != null ? obj.toString() : null;
	}

	/**
	 * 封裝和jedis方法
	 *
	 * @param key
	 * @param value
	 * @return
	 */
	private boolean setNX(final String key, final String value) {
		return redisTemplate.opsForValue().setIfAbsent(key, value);
	}

	/**
	 * 封裝和jedis方法
	 *
	 * @param key
	 * @param value
	 * @return
	 */
	private String getSet(final String key, final String value) {
		Object obj = redisTemplate.opsForValue().getAndSet(key, value);
		return obj != null ? (String) obj : null;
	}


	//=================================公共方法區============================//

	/**
	 * 初始化參數
	 * 默認鎖超時時間 2分鐘
	 * 默認獲取鎖等待時間 10秒
	 *
	 * @param lockKey 鎖的key
	 */
	public void init(String lockKey) {
		this.lockKey = lockKey + LOCK_SUFFIX;
	}

	/**
	 * 初始化參數
	 *
	 * @param lockKey      鎖的key
	 * @param timeoutMsecs 獲取鎖的超時時間 單位毫秒 eg:2 * 60 * 1000
	 */
	public void init(String lockKey, int timeoutMsecs) {
		this.lockKey = lockKey + LOCK_SUFFIX;
		this.timeoutMsecs = timeoutMsecs;
	}

	/**
	 * 初始化參數
	 *
	 * @param lockKey      鎖的key
	 * @param timeoutMsecs 獲取鎖的超時時間
	 * @param expireMsecs  鎖的有效期 單位毫秒 eg: 10 * 1000
	 */
	public void init(String lockKey, int timeoutMsecs, int expireMsecs) {
		this.lockKey = lockKey + LOCK_SUFFIX;
		this.timeoutMsecs = timeoutMsecs;
		this.expireMsecs = expireMsecs;
	}

	public String getLockKey() {
		return lockKey;
	}

	/**
	 * 獲取鎖
	 *
	 * @return 獲取鎖成功返回ture,超時返回false
	 * @throws InterruptedException
	 */
	public synchronized boolean lock() throws InterruptedException {
		int timeout = timeoutMsecs;
		logger.info("準備獲取鎖{},獲取鎖超時時間爲{}",this.getLockKey(),timeout);
		while (timeout >= 0) {
			long expires = System.currentTimeMillis() + expireMsecs + 1;
			String expiresStr = String.valueOf(expires); // 鎖到期時間
			if (this.setNX(lockKey, expiresStr)) {
				locked = true;
				logger.info("成功獲取鎖{},鎖超時時間爲{}",this.getLockKey(),expires);
				return true;
			}
			// redis裏key的時間
			String currentValue = this.get(lockKey);
			// 判斷鎖是否已經過期,過期則重新設置並獲取
			if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
				// 設置鎖並返回舊值
				String oldValue = this.getSet(lockKey, expiresStr);
				// 比較鎖的時間,如果不一致則可能是其他鎖已經修改了值並獲取
				if (oldValue != null && oldValue.equals(currentValue)) {
					logger.info("成功獲取鎖{},鎖超時時間爲{}",this.getLockKey(),expires);
					locked = true;
					return true;
				}
			}
			timeout -= DEFAULT_ACQUIRY_RETRY_MILLIS;
			// 延時
			Thread.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
		}
		logger.warn("獲取鎖{}失敗了!",this.getLockKey());
		return false;
	}

	/**
	 * 釋放獲取到的鎖
	 */
	public void unlock() {
		logger.info("準備釋放鎖{}",this.getLockKey());
		if (locked) {
			redisTemplate.delete(lockKey);
			logger.info("成功釋放鎖{}",this.getLockKey());
			locked = false;
		}
	}

}

4.redis緩存數據測試

具體類無需關心,只要看註解如何使用即可。

//REDIS緩存調用示例

@Deprecated
@Service
@CacheConfig(cacheNames = "user") //指定cache的名字,這裏指定了 caheNames,下面的方法的註解裏就可以不用指定 value 屬性了
public class UserServiceImpl {

	private Map<Long, User> userMap = new HashMap<Long, User>();

	public UserServiceImpl() {
		User u1=new User();
		u1.setId(1L);
		u1.setName("1111");
		u1.setPassword("11223434");
		User u2=new User();
		u2.setId(2L);
		u2.setName("1111");
		u2.setPassword("11223434");
		User u3=new User();
		u3.setId(3L);
		u3.setName("1111");
		u3.setPassword("11223434");

		userMap.put(1L,u1);
		userMap.put(2L,u2);
		userMap.put(3L,u3);
	}

	@Cacheable()
	@Override
	public List<User> list() {
		System.out.println("querying list.....");
		User[] users = new User[userMap.size()];
		this.userMap.values().toArray(users);
		return Arrays.asList(users);
	}

	@Cacheable(key = "'user:'.concat(#id.toString())")
	@Override
	public User findUserById(Long id) {
		System.out.println("沒有調用緩存");
		return userMap.get(id);
	}


	@Cacheable(key = "'user:'.concat(#user.id)")
	@Override
	public void update(User user) {
		System.out.println("沒有調用緩存");
		userMap.put(user.getId(), user);
	}

	// 清空cache
	@CacheEvict(key = "'user:'.concat(#id.toString())")
	@Override
	public void remove(Long id) {
		System.out.println("沒有調用緩存");
		userMap.remove(id);
	}

	@CacheEvict(key = "'user:'.concat(#id.toString())")
	@Override
	public User upuser(Long id) {
		System.out.println("沒有調用緩存");
		User d = userMap.get(id);
		d.setName("0000000000000000000000000000000000000000");
		return d;
	}

	@CachePut(key = "'user:'.concat(#user.id)")
	@Override
	public User saveUser(User user) {
		System.out.println("沒有調用緩存");
		userMap.put(user.getId(), user);
		return user;
	}

}

5.Lock嵌入添加示例以及測試

以下無需關注具體業務,只需看lock如何嵌入使用

@Transactional
@Service
public class TestSrvImpl implements TestSrv {
	Logger log = LoggerFactory.getLogger(this.getClass());

	@Resource
	private RedisLock redisLock; //這裏自動注入redisLock類

	@Resource
	private UserInfoRepository userInfoRepository;

	@LogAnnotation(moduleName = "測試服務", option = "測試方法")
	public ResponseVO testService(RequestVO requestVO) {
		//獲取支付配置相關信息
		ResponseVO response = new ResponseVO(requestVO);
		try {
			log.info("準備獲取鎖...");
			redisLock.init("Test",20*1000);
			if (redisLock.lock()) {
				log.info("成功獲取鎖,入參:{}", JSON.toJSONString(requestVO));
				Map<String, Object> resultMap = new HashMap<>();
				resultMap.put("no", "123456");
				resultMap.put("name", "test");
				HandlerStateDTO handlerStateDTO = new HandlerStateDTO();
				response.setBizObj(resultMap);
				if (!HandlerType.isSuccessful(handlerStateDTO)) {
					response.setRetInfo(handlerStateDTO);
				}
				log.info("開始進入睡眠模式");
				Thread.sleep(5000);
				log.info("睡眠模式解除");
				redisLock.unlock();
				log.info("釋放鎖");
			}
		} catch (Exception e) {
			e.printStackTrace();
			//異常釋放鎖
			redisLock.unlock();
			response.setRetInfo(HandlerType.UNKNOWN);
			log.error("失敗了,{}" + e.toString());
		}
		log.info("結束返回");
		return response;
	}

本樣例SDK模擬多線程測試用例如下,自己根據自己的服務調用方式模擬調用即可。本項目採用sdk請求http接口進行調用:

/**
	 * 測試
	 */
	@Test
	public void test() {
		logger.info("開始請求");
		SdkClient sdkClient = new SdkClient(web_pay_url, appId, appSecret, signType, encryptType);
		try {
			Map<String,Object> param = new HashMap<>();
			param.put("name","testUser");
			param.put("id","A000001");
//			// 發起交易
			ResponseVO responseParams = sdkClient.unifyCall("test.service.forfun",version,param);
			logger.info("返回的結果爲:"+JSON.toJSONString(responseParams));
			logger.info("其中業務返回爲:"+JSON.toJSONString(responseParams.getBizObj()));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
    @Test
	public void test3() throws InterruptedException {
		// 同一時刻最大的併發線程的個數  即併發數
		final int concurrentThreadNum = 2;

		//總共多少線程
		final int countThreadNum = 2;

		ExecutorService executorService = Executors.newCachedThreadPool();
		CountDownLatch countDownLatch = new CountDownLatch(countThreadNum);
		Semaphore semaphore = new Semaphore(concurrentThreadNum);
		for (int i = 0; i< countThreadNum; i++) {
			executorService.execute(()->{
				try {
					semaphore.acquire();
					test();
					semaphore.release();
				} catch (InterruptedException e) {
					logger.error("exception", e);
				}
				countDownLatch.countDown();
			});
		}
		countDownLatch.await();
		executorService.shutdown();
		logger.info("請求完成");
	}

6.基礎框架項目

內置spring boot 和接口加解密sdk,redis鎖等,下載即可開發具體業務

碼雲:BaseFramework

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章