场景:
程序中插入数据前,逻辑性先判断数据是否存在,存在则做更新,不存在则插入,在高并发的场景先,多个线程在同一个时间节点来访问此程序代码,会同时发出多条插入语句,违反了实际应用逻辑;
思考解决方案:
1、sychronized是线程锁,单机应用可解决,分布式并发中无法保障逻辑正常,其无法解决上述问题;
2、数据库层面上设置锁,或者把数据的某个自动设唯一,可以解决上述问题,但是在数据库层面加锁会极大的影响应用程序性能(并且某些特定数据结构无法做相应设计);
分布式锁:控制多个进程在多个节点中,对应用资源的访问;
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;
基于redis实现分布式锁:
1.引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 切面编程:切面依赖,通过切面和注解来简化逻辑,进来少的侵入业务代码--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency>
2.配置application.properties:
spring.redis.database=0 spring.redis.host=192.168.1.11 spring.redis.port=10179 spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle =0 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait=-1
3.创建RedisLockInfo实体类:
package com.cc.springbootredislock.lock; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class RedisLockInfo { //UUID private String lockId; //REDIS KEY private String redisKey; //过期时间 private Long expire; //尝试获取锁超时时间 private Long tryTimeout; //尝试获取锁次数 private int tryCount; }
4.创建声明锁类RedisLock
package com.cc.springbootredislock.lock; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(value = {ElementType.METHOD}) @Retention(value = RetentionPolicy.RUNTIME) public @interface RedisLock { /** * REDIS KEY * @return */ String[] keys() default ""; /** * 过期时间 * @return */ long expire() default Long.MIN_VALUE; /** * 尝试获取锁超时时间 * @return */ long tryTimeout() default Long.MIN_VALUE; }
5.创建检测方法是否锁注释类RedisKeyGenerator
package com.cc.springbootredislock.lock; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.EvaluationContext; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; /** * */ public class RedisKeyGenerator { private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); private static final ExpressionParser PARSER = new SpelExpressionParser(); public String getKeyName(ProceedingJoinPoint joinPoint, RedisLock redisLock) { List<String> keyList = new ArrayList<>(); Method method = getMethod(joinPoint); List<String> definitionKeys = getSpelDefinitionKey(redisLock.keys(), method, joinPoint.getArgs()); keyList.addAll(definitionKeys); return StringUtils.collectionToDelimitedString(keyList,"","",""); } private List<String> getSpelDefinitionKey(String[] definitionKeys, Method method, Object[] parameterValues) { List<String> definitionKeyList = new ArrayList<String>(); for (String definitionKey : definitionKeys) { if (definitionKey != null && !definitionKey.isEmpty()) { EvaluationContext context = new MethodBasedEvaluationContext(null, method, parameterValues, NAME_DISCOVERER); String key = PARSER.parseExpression(definitionKey).getValue(context).toString(); definitionKeyList.add(key); } } return definitionKeyList; } private Method getMethod(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); if (method.getDeclaringClass().isInterface()) { try { method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), method.getParameterTypes()); } catch (Exception e) { e.printStackTrace(); } } return method; } }
6.创建锁工厂类RedisLockFactory
package com.cc.springbootredislock.factory; import com.cc.springbootredislock.lock.RedisLockInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.util.Assert; import java.util.Collections; import java.util.UUID; public class RedisLockFactory { @Autowired private RedisTemplate redisTemplate; //PS:使用Jedis在cluster(集群环境下)是不支持lua脚本,会报错:EvalSha is not supported in cluster environment. //不使用Jedis来连接,可以通过lettuce来使用,lettuce支持luo脚本 private static final String LUA_SCRIPT_LOCK = "return redis.call('set',KEYS[1],ARGV[1],'NX','PX',ARGV[2])"; private static final RedisScript<String> SCRIPT_LOCK = new DefaultRedisScript<>(LUA_SCRIPT_LOCK,String.class); private static final String LUA_SCRIPT_UNLOCK = "if redis.call('get',KEYS[1]) == ARGV[1] then return tostring(redis.call('del', KEYS[1])) else return '0' end"; private static final RedisScript<String> SCRIPOT_UNLOCK = new DefaultRedisScript<>(LUA_SCRIPT_UNLOCK,String.class); /** * 加锁方法 * @param redisKey 缓存KEY * @param expire 到期时间 毫秒 * @param tryTimeout 尝试获取锁超时时间 毫秒 *@return * 加锁的逻辑要保障原子性,所有使用lua脚本来保障获取锁的原子性 */ public RedisLockInfo tryLock(String redisKey, long expire, long tryTimeout){ Assert.isTrue(tryTimeout > 0,"tryTimeout必须大于0"); long timestamp = System.currentTimeMillis(); int tryCount = 0; String lockId = UUID.randomUUID().toString(); while ((System.currentTimeMillis() - timestamp) < tryTimeout){ try { Object lockResult = redisTemplate.execute(SCRIPT_LOCK, //执行lua脚本 redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(redisKey), lockId,String.valueOf(expire)); tryCount++; if (null != lockResult && "OK".equals(lockResult)){ return new RedisLockInfo(lockId,redisKey,expire,tryTimeout,tryCount); }else{ Thread.sleep(50); } }catch(Exception e){ e.printStackTrace(); } } return null; } /** * 解锁 * @param redisLockInfo 获取锁返回的对象 * @return */ public boolean releaseLock(RedisLockInfo redisLockInfo){ Object releaseResult = null; try { releaseResult = redisTemplate.execute(SCRIPOT_UNLOCK, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(redisLockInfo.getRedisKey()), redisLockInfo.getLockId()); }catch (Exception e){ e.printStackTrace(); } return null != releaseResult && releaseResult.equals(1); } }
7.创建切面类RedisLockAspect
package com.cc.springbootredislock.aspect; import com.cc.springbootredislock.factory.RedisLockFactory; import com.cc.springbootredislock.lock.RedisKeyGenerator; import com.cc.springbootredislock.lock.RedisLock; import com.cc.springbootredislock.lock.RedisLockInfo; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * */ @Aspect @Component @Slf4j public class RedisLockAspect { @Autowired private RedisLockFactory redisLockFactory; @Autowired private RedisKeyGenerator redisKeyGenerator; @Around("@annotation(redisLock)") public Object around(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable { RedisLockInfo redisLockInfo = null; try { String keyName = redisKeyGenerator.getKeyName(point, redisLock); redisLockInfo = redisLockFactory.tryLock(keyName, redisLock.expire(), redisLock.tryTimeout()); if (null != redisLockInfo) { Object result = point.proceed(); return result; } } catch (Throwable e) { log.error("around exception", e); throw e; } finally { if (null != redisLockInfo) {//主动释放锁 redisLockFactory.releaseLock(redisLockInfo); } } return null; } }
8.创建应用切面生产redis锁
package com.cc.springbootredislock.config; import com.cc.springbootredislock.aspect.RedisLockAspect; import com.cc.springbootredislock.factory.RedisLockFactory; import com.cc.springbootredislock.lock.RedisKeyGenerator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @AutoConfigureAfter(RedisLockAutoConfiguration.class) //指定在RedisLockAutoConfiguration初始化后在初始化RedisLockAspect,RedisLockAspect需要依赖RedisLockAutoConfiguration中的对象 @Import(RedisLockAspect.class) //导入切面,通知spring容器RedisLockAspect切面的存在 public class RedisLockAutoConfiguration { @Bean public RedisLockFactory redisLockFactory(){ return new RedisLockFactory(); } @Bean public RedisKeyGenerator redisKeyGenerator(){ return new RedisKeyGenerator(); } }
9.service
package com.cc.springbootredislock.service; import com.cc.springbootredislock.lock.RedisLock; import org.springframework.stereotype.Service; @Service public class LockService { @RedisLock(keys = "#id",expire = 30000,tryTimeout = 1000) public String test(String id){ return id; } }
10.controller
package com.cc.springbootredislock.controller; import com.cc.springbootredislock.service.LockService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.UUID; @RestController public class LockController { @Resource private LockService lockService; @RequestMapping("lock") public String lock(){ String id = UUID.randomUUID().toString(); lockService.test(id); return new String(id); } }
使用debug启动,断点测试来查看运行结果。