基於注解的方式實現分布式鎖


基於注解的方式實現分布式鎖

關於分布式鎖的實現由兩種 1. 基於redis 2. 基於zookeeper

為了方便分布式鎖的使用, 基於注解的方式抽取成公用組件

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的切面

/**
 * 處理@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分布式鎖的實現

/**
 * 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;
    }

}

ErrorCodeEnum

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;

}

配置文件

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");
        }
    }

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM