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