基於註解的方式實現分佈式鎖

基於註解的方式實現分佈式鎖

關於分佈式鎖的實現由兩種 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");
        }
    }

}
相關文章
相關標籤/搜索