关于分布式锁的实现由两种 1. 基于redis 2. 基于zookeeperredis
为了方便分布式锁的使用, 基于注解的方式抽取成公用组件spring
DisLock注解缓存
/** * 分布式锁的注解, 经过指定key做为分布式锁的key * * @author wang.js on 2019/1/29. * @version 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DisLock { /** * 分布式锁的key * * @return */ String key(); /** * 分布式锁用的业务场景id * * @return */ String biz(); /** * 过时时间, 默认是5秒 * 单位是秒 * * @return */ int expireTime() default 5; }
处理DisLock的切面springboot
/** * 处理@DisLock注解的切面 * * @author wang.js on 2019/1/29. * @version 1.0 */ @Aspect @Order(value = 1) @Component public class DisLockAspect { @Resource private DisLockUtil disLockUtil; private static final int MIN_EXPIRE_TIME = 3; @Around(value = "@annotation(disLock)") public Object execute(ProceedingJoinPoint proceedingJoinPoint, DisLock disLock) throws Throwable { int expireTIme = disLock.expireTime() < MIN_EXPIRE_TIME ? MIN_EXPIRE_TIME : disLock.expireTime(); String disKey = CacheKeyParser.parse(proceedingJoinPoint, disLock.key(), disLock.biz()); boolean lock = disLockUtil.lock(disKey, expireTIme); int count = 1; while (!lock && count < MIN_EXPIRE_TIME) { lock = disLockUtil.lock(disKey, expireTIme); count++; TimeUnit.SECONDS.sleep(1); } Object proceed; if (lock) { // 容许查询 try { proceed = proceedingJoinPoint.proceed(); } finally { // 删除分布式锁 disLockUtil.unlock(disKey, false); } } else { throw new CustomException(ErrorCodeEnum.DUPLICATE_REQUEST.getMessage()); } return proceed; } }
redis的配置服务器
/** * @author wang.js * @date 2018/12/17 * @copyright yougou.com */ @Configuration public class RedisConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port:6379}") private Integer port; @Bean public JedisPool jedisPool() { //1.设置链接池的配置对象 JedisPoolConfig config = new JedisPoolConfig(); //设置池中最大链接数 config.setMaxTotal(50); //设置空闲时池中保有的最大链接数 config.setMaxIdle(10); config.setMaxWaitMillis(3000L); config.setTestOnBorrow(true); //2.设置链接池对象 return new JedisPool(config,host,port); } }
redis分布式锁的实现app
/** * redis分布式锁的实现 * * @author wang.js * @date 2018/12/18 * @copyright yougou.com */ @Component public class DisLockUtil { @Resource private JedisPool jedisPool; private static final int DEFAULT_EXPIRE_TIME = 5; private static final Long RELEASE_SUCCESS = 1L; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } /** * 释放分布式锁 * * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } /** * 释放锁 * * @param key * @return */ public final boolean unlock(String key, boolean needCheck) { boolean result = false; Jedis jedis = jedisPool.getResource(); try { if (needCheck) { String expireTimeCache = jedis.get(key); // 判断锁是否过时了 if (StringUtils.isBlank(expireTimeCache)) { result = true; } if (System.currentTimeMillis() - Long.parseLong(expireTimeCache) > 0) { // 直接删除 jedis.del(key); result = true; } } else { jedis.del(key); } } finally { jedis.close(); } return result; } /** * 获取分布式锁 * * @param key * @param expireSecond * @return */ public final boolean lock(String key, int expireSecond) { if (StringUtils.isBlank(key)) { throw new RuntimeException("传入的key为空"); } expireSecond = expireSecond == 0 ? DEFAULT_EXPIRE_TIME : expireSecond; // 过时的时候的时间戳 long expireTime = System.currentTimeMillis() + expireSecond * 1000 + 1; boolean setResult = false; Jedis jedis = jedisPool.getResource(); try { if (jedis.setnx(key, String.valueOf(expireTime)) == 1) { // 说明加锁成功 setResult = true; } if (jedis.ttl(key) < 0) { jedis.expire(key, expireSecond); } if (setResult) { return true; } String expireTimeCache = jedis.get(key); System.out.println(expireTimeCache + "====" + jedis.ttl(key) + ", now:" + System.currentTimeMillis()); // 判断锁是否过时了 if (StringUtils.isNotBlank(expireTimeCache) && System.currentTimeMillis() - Long.parseLong(expireTimeCache) > 0) { String oldExpireTime = jedis.getSet(key, String.valueOf(expireTime)); if (StringUtils.isNotBlank(oldExpireTime) && oldExpireTime.equals(String.valueOf(expireTime))) { jedis.expire(key, expireSecond); setResult = true; } } } finally { jedis.close(); } return setResult; } }
实现分布式锁的关键是对key的设置, 须要获取实际的参数来设置分布式锁, 这里自定义了解析器分布式
/** * cache key 的解析器 * * @author wang.js on 2019/2/27. * @version 1.0 */ public class CacheKeyParser { /** * 解析缓存的key * * @param proceedingJoinPoint 切面 * @param cacheKey 缓存的key * @param biz 业务 * @return String * @throws IllegalAccessException 异常 */ public static String parse(ProceedingJoinPoint proceedingJoinPoint, String cacheKey, String biz) throws IllegalAccessException { // 解析实际参数的key String key = cacheKey.replace("#", ""); StringTokenizer stringTokenizer = new StringTokenizer(key, "."); Map<String, Object> nameAndValue = getNameAndValue(proceedingJoinPoint); Object actualKey = null; while (stringTokenizer.hasMoreTokens()) { if (actualKey == null) { actualKey = nameAndValue.get(stringTokenizer.nextToken()); } else { actualKey = getPropValue(actualKey, stringTokenizer.nextToken()); } } return biz + actualKey; } /** * 获取参数Map集合 * * @param joinPoint 切面 * @return Map<String, Object> */ private static Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) { Object[] paramValues = joinPoint.getArgs(); String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames(); Map<String, Object> param = new HashMap<>(paramNames.length); for (int i = 0; i < paramNames.length; i++) { param.put(paramNames[i], paramValues[i]); } return param; } /** * 获取指定参数名的参数值 * * @param obj * @param propName * @return * @throws IllegalAccessException */ public static Object getPropValue(Object obj, String propName) throws IllegalAccessException { Field[] fields = obj.getClass().getDeclaredFields(); for (Field f : fields) { if (f.getName().equals(propName)) { //在反射时能访问私有变量 f.setAccessible(true); return f.get(obj); } } return null; } }
ErrorCodeEnumide
public enum ErrorCodeEnum { SUCCESS("查询成功", "200"), SERVER_ERROR("服务器异常", "500"), SECKILL_END("秒杀活动已结束", "250"), GOODS_KILLED("秒杀成功", "502"), ERROR_SIGN("签名不合法", "260"), UPDATE_SUCCESS("更新成功", "0"), SAVE_SUCCESS("保存成功", "0"), UPDATE_FAIL("更新失败", "256"), EMPTY_PARAM("参数为空", "257"), SAVE_ERROR("保存失败", "262"), SERVER_TIMEOUT("调用超时", "501"), USER_NOT_FOUND("找不到用户", "502"), COUPON_NOT_FOUND("找不到优惠券", "503"), DUPLICATE("出现重复", "504"), USER_STATUS_ABNORMAL("用户状态异常", "505"), NO_TOKEN("无token,请从新登陆", "506"), ERROR_TOKEN("token不合法", "507"), EMPTY_RESULT("暂无数据", "508"), DUPLICATE_REQUEST("重复请求", "509"), ; /** * 定义的message */ private String message; /** * 定义的错误码 */ private String errCode; ErrorCodeEnum(String message, String errCode) { this.message = message; this.errCode = errCode; } public String getMessage() { return message; } protected void setMessage(String message) { this.message = message; } public String getErrCode() { return errCode; } protected void setErrCode(String errCode) { this.errCode = errCode; } }
自定义异常CustomException测试
/** * @author Eric on 2018/12/24. * @version 1.0 */ @Data @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) public class CustomException extends RuntimeException { private String message; }
配置文件this
spring: redis: host: mini7 port: 6379
定义一个方法, 加上@RedisCache注解, cacheKey的值必须是#实际参数名.属性名的格式, 若是想要成其余的格式能够修改CacheKeyParser中的parse方法
@DisLock(key = "#id", biz = CommonBizConstant.SECOND_KILL)
@Override public String testRedisCache(String id) { LOGGER.info("调用方法获取值"); return "大傻逼"; }
在springboot启动类上加上@ComponentScan({"com.eric"})
/** * @author Eric on 2019/1/26. * @version 1.0 */ @SpringBootApplication @MapperScan("com.eric.base.data.dao") @ComponentScan({"com.eric"}) @EnableFeignClients @EnableDiscoveryClient public class BaseDataApplication { public static void main(String[] args) { SpringApplication.run(BaseDataApplication.class, args); } }
写个测试类调用上面的方法
/** * 基础数据 * * @author wang.js on 2019/2/27. * @version 1.0 */ @SpringBootTest @RunWith(SpringRunner.class) public class BaseDataTest { @Resource private SysDictService sysDictService; @Test public void t1() { for (int i = 0; i < 100; i++) { sysDictService.testRedisCache("1"); } } }