分布式锁:锁了,就只有锁定的线程才能操作。
与java中的锁类似,只是我们是否锁定是依托与第三方redis中的一个key标识判断是否可以操作。
现在场景是:一个订单来了,必须处理,等待上个线程处理完后,竞争取得锁,否则就处理超时,业务处理失败。
下面是锁的工具类:
很奇怪的是,取不到锁时,等待期间不能休眠,否则还是锁不住。。所以就不休眠,作死了遍历查询。
import java.util.Arrays; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import lombok.extern.slf4j.Slf4j; /** * @Desc : 分布式锁 * @Company : 晨创科技 * @author : Chenweixian 陈惟鲜 * @Date : 2018年6月5日 下午2:41:03 */ @Slf4j public class RedisLockUtil { private static final Long RELEASE_SUCCESS = 1L; private static final StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) SpringUtils.getBean("stringRedisTemplate"); private static final DefaultRedisScript<Long> redisScript = (DefaultRedisScript<Long>) SpringUtils.getBean("redisScript"); private static long timeout = 65 * 1000L; // 锁定65S /** * 上锁 * @param key 锁标识 * @param value 线程标识 * @return 上锁状态 */ public static boolean lock(String key, String value, long mytimeout) { long start = System.currentTimeMillis(); // 检测是否超时 if (System.currentTimeMillis() - start > mytimeout) { return false; } // 执行set命令 Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, value, mytimeout, TimeUnit.MILLISECONDS);// 毫秒 // 是否成功获取锁 if (absent) { return true; } return false; } /** * 上锁 * @param key 锁标识 * @param value 线程标识 * @return 上锁状态 */ public static boolean lock(String key, String value) { long start = System.currentTimeMillis(); while (true) { // 检测是否超时 if (System.currentTimeMillis() - start > timeout) { return false; } // 执行set命令 Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);// 毫秒 // 是否成功获取锁 if (absent) { return true; } // 不能休眠 // try { // Thread.sleep(5);// 等待50毫秒 // } catch (InterruptedException e) { // } } } /** * 解锁 * @param key 锁标识 * @param value 线程标识 * @return 解锁状态 */ public static boolean unlock(String key, String value) { // 使用Lua脚本:先判断是否是自己设置的锁,再执行删除 Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, value)); // 返回最终结果 return RELEASE_SUCCESS.equals(result); } }
调用方法:
做了个注解@RedisLock,意味着,加上这个注解后,都会取锁后才能执行。
这里是针对加上注解的类方法锁定
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; /** * @Desc : 分布式锁切面拦截,方法上加上@RedisLock即可 * @author : Chenweixian 陈惟鲜 * @Date : 2018年5月7日 下午7:47:56 */ @Slf4j @Aspect @Component @Order(7) // @Order(1)数字越小优先级越高 public class RedisLockAspect { /**拦截所有controller包下的方法*/ @Pointcut("@annotation(com.ccjr.commons.annotation.RedisLock)") private void lockMethod(){ } /** * 日志打印 * @author : 陈惟鲜 chenweixian * @Date : 2018年8月8日 下午5:29:47 * @param point * @return * @throws Throwable */ @Around("lockMethod() && @annotation(redisLock)") public Object doAround(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable { String class_name = point.getTarget().getClass().getName(); String method_name = point.getSignature().getName(); String threadUUID = UUIDUtils.generateUUID64(); String redisKey = RedisConstants.keyRedisLock(class_name + "." + method_name); Object result = null; try { RedisLockUtil.lock(redisKey, threadUUID); log.info("lock aop rediskey :"+redisKey); result = point.proceed();// result的值就是被拦截方法的返回值 } finally { RedisLockUtil.unlock(redisKey, threadUUID); log.info("unlock aop rediskey :"+redisKey); } return result; } }
注解类RedisLock.java
/** * @Desc : redisLock注解标签,使用该标签后,并且在app中配置RedisLockAop后,项目将实现注解分布式锁, * key为当前类+方法名 * @author : Chenweixian 陈惟鲜 * @Date : 2018年5月7日 下午7:44:56 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RedisLock { }
调用时就简单了:
@Override @RedisLock public BaseResponse<String> genHisTask(BaseRequest<GenHisAssetsAnalyzeTaskReq> baseRequest) { String msg = "生成历史数据"; // 业务处理过程。。。。。。return baseApiService.setResultSuccess(); }
当然如果针对某块来锁定的,比如锁定某个客户的库存,
客户买入时冻结钱,商家卖出时冻结商品,针对一个商品号goodsId锁定
String threadUUID = UUIDUtils.generateUUID64(); String redisKey = RedisConstants.keyRedisLockCash(goodsId); try { if (!RedisLockUtil.lock(redisKey, threadUUID)) { throw new BusinessBizException(ApiErrorEnum.TRADE_CASH_21000, goodsId); } // 执行业务过程。。。。。。 results.add(result); } finally { RedisLockUtil.unlock(redisKey, threadUUID); }
使用的是springboot2,采用代码配置式。
RedisConfig.java
import java.time.Duration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.springframework.cache.CacheManager; 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.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { /** * json序列化 * * @return */ @Bean public RedisSerializer<Object> jackson2JsonRedisSerializer() { // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); return serializer; } /** * 配置缓存管理器 * * @param redisConnectionFactory * @return */ @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 生成一个默认配置,通过config对象即可对缓存进行自定义配置 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 设置缓存的默认过期时间,也是使用Duration设置 config = config.entryTtl(Duration.ofMinutes(1)) // 设置 key为string序列化 .serializeKeysWith( RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置value为json序列化 .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer())) // 不缓存空值 .disableCachingNullValues(); // 设置一个初始化的缓存空间set集合 Set<String> cacheNames = new HashSet<>(); cacheNames.add("timeGroup"); cacheNames.add("user"); // 对每个缓存空间应用不同的配置 Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("timeGroup", config); configMap.put("user", config.entryTtl(Duration.ofSeconds(120))); // 使用自定义的缓存配置初始化一个cacheManager RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) // 一定要先调用该方法设置初始化的缓存名,再初始化相关的配置 .initialCacheNames(cacheNames).withInitialCacheConfigurations(configMap).build(); return cacheManager; } @Bean public RedisTemplate<String, String> StringRedisTemplate(RedisConnectionFactory factory) { StringRedisTemplate template = new StringRedisTemplate(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } /** * @return lua脚本===================重点 */ @Bean public DefaultRedisScript<Long> redisScript() { DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(Long.class); defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end"); return defaultRedisScript; } }