基于注解的方式实现分布式锁
基于注解的方式实现分布式锁
关于分布式锁的实现由两种 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");
}
}
}