Distributed lock based on annotation

Implementing distributed locks based on annotations

There are two implementations of distributed locks: 1. Based on redis 2. Based on zookeeper

For convenience The use of distributed locks is extracted into public components based on annotations

DisLock annotations

/**

* The annotation of the distributed lock, by specifying the key as the key of the distributed lock
*
* @author wang.js on 2019/1/29.
* @version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DisLock {

/**
* The key of the distributed lock
*
* @return
*/
String key();

/**
* Business scenario id for distributed lock
*
* @return
*/
String biz();

/**
* Expiration time, the default is 5 seconds
* The unit is seconds
*
* @return
*/
int expireTime() default 5;

}

Processing aspects of DisLock

/**

* Processing @DisLock annotation aspect
*
* @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() String disKey = CacheKeyParser.parse(proceedingJoinPoint, disLock.key(), disLock.biz());
boolean lock = disLockUtil.lock(disKey, expireTIme);
int count = 1;
while (!lock && count lock = disLockUtil.lock(disKey, expireTIme);
count++;
TimeUnit.SECONDS.sleep(1);
}
Object proceed;
if (lock) {
// Allow query
try {
proceed = proceedingJoinPoint.proceed();
} finally {
// Delete distributed lock
disLockUtil.unlock(disKey, false);
}
} else {
throw new CustomException(ErrorCodeEnum.DUPLICATE_REQUEST.getMessage());
}
return proceed;
}

}

Redis configuration

/**

* @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. Set the configuration object of the connection pool
JedisPoolConfig config = new JedisPoolConfig();
//Set the maximum number of connections in the pool
config.setMaxTotal(50);
//Set the maximum number of connections held in the pool when idle
config.setMaxIdle(10);
config.setMaxWaitMillis(3000L);
config.setTestOnBorrow(true);
//2. Set the connection pool object
return new JedisPool(config,host,port);
}
}

Realization of redis distributed lock

/**

* Implementation of redis distributed lock
*
* @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";

/**
* Try to acquire a distributed lock
*
* @param jedis Redis client
* @param lockKey lock
* @param requestId Request ID
* @param expireTime expired time
* @return whether the acquisition is successful
*/
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;
}

/**
* Release the distributed lock
*
* @param jedis Redis client
* @param lockKey lock
* @param requestId Request ID
* @return Whether the release was successful
*/
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;
}

/**
* Release the lock
*
* @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);
// Determine whether the lock has expired
if (StringUtils.isBlank(expireTimeCache)) {
result = true;
}
if (System.currentTimeMillis()-Long.parseLong(expireTimeCache)> 0) {
// delete directly
jedis.del(key);
result = true;
}
} else {
jedis.del(key);
}
} finally {
jedis.close();
}
return result;
}

/**
* Obtain distributed lock
*
* @param key
* @param expireSecond
* @return
*/
public final boolean lock(String key, int expireSecond) {
if (StringUtils.isBlank(key)) {
throw new RuntimeException("The key passed in is empty");
}
expireSecond = expireSecond == 0? DEFAULT_EXPIRE_TIME: expireSecond;
// Time stamp when it expires
long expireTime = System.currentTimeMillis() + expireSecond * 1000 + 1;
boolean setResult = false;
Jedis jedis = jedisPool.getResource();
try {
if (jedis.setnx(key, String.valueOf(expireTime)) == 1) {
// indicates that the lock is successful
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());
// Determine whether the lock has expired
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;
}

}

The key to implementing distributed locks is to set the key. You need to obtain the actual parameters to set the distributed locks. Here, the parser is customized

/* *

* Cache key parser
*
* @author wang.js on 2019/2/27.
* @version 1.0
*/
public class CacheKeyParser {

/**
* Parse the cached key
*
* @param proceedingJoinPoint section
* @param cacheKey cache key
* @param biz business
* @return String
* @throws IllegalAccessException
*/
public static String parse(ProceedingJoinPoint proceedingJoinPoint, String cacheKey, String biz) throws IllegalAccessException {
// parse the key of the actual parameter
String key = cacheKey.replace("#", "");
StringTokenizer stringTokenizer = new StringTokenizer(key, ".");

Map 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;
}

/**
* Get parameter Map collection
*
* @param joinPoint section
* @return Map
*/
private static Map getNameAndValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues ​​= joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map param = new HashMap<>(paramNames.length);

for (int i = 0; i param.put(paramNames[i], paramValues[i]);
}
return param;
}

/**
* Get the parameter value of the specified parameter name
*
* @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)) {
//Private variables can be accessed during reflection
f.setAccessible(true);
return f.get(obj);
}
}
return null;
}

}

ErrorCodeEnum

public enum ErrorCodeEnum {


SUCCESS("Query successful", "200"),
SERVER_ERROR("Server Exception", "500"),
SECKILL_END("The spike activity has ended", "250"),
GOODS_KILLED("Successful second kill", "502"),
ERROR_SIGN("Illegal signature", "260"),
UPDATE_SUCCESS("Update successful", "0"),
SAVE_SUCCESS("Save successfully", "0"),
UPDATE_FAIL("Update failed", "256"),
EMPTY_PARAM("Parameter is empty", "257"),
SAVE_ERROR("Save failed", "262"),
SERVER_TIMEOUT("Call timeout", "501"),
USER_NOT_FOUND("User not found", "502"),
COUPON_NOT_FOUND("Coupon not found", "503"),
DUPLICATE("Duplicate", "504"),
USER_STATUS_ABNORMAL("User Status Abnormal", "505"),
NO_TOKEN("No token, please log in again", "506"),
ERROR_TOKEN("token is illegal", "507"),
EMPTY_RESULT("No data yet", "508"),
DUPLICATE_REQUEST("Duplicate Request", "509"),
;

/**
* defined message
*/
private String message;
/**
* Defined error code
*/
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;

}

Configuration file

spring:

redis:
host: mini7
port: 6379

Test

Define a method, add @RedisCache annotation, the value of cacheKey must be #actual parameter name. The format of the attribute name, if you want You can modify the parse method in CacheKeyParser into other formats

@DisLock(key = "#id", biz = CommonBizConstant.SECOND_KILL)
@Override public String testRedisCache(String id) {LOGGER.info("Call method to get value"); return "Dabo"; }

Add @ComponentScan({“com.eric”}) to the springboot startup class

< pre>/**

* @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);

}

}

Write a test class to call the above method

/**

* Basic data
*
* @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");
}
}

}

Implementing distributed locks based on annotations

There are two types of distributed locks: 1. Based on redis 2. Based on zookeeper

In order to facilitate the use of distributed locks, extract them into public components based on annotations

DisLock annotations

/**

* The annotation of the distributed lock, by specifying the key as the key of the distributed lock
*
* @author wang.js on 2019/1/29.
* @version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DisLock {

/**
* The key of the distributed lock
*
* @return
*/
String key();

/**
* Business scenario id for distributed lock
*
* @return
*/
String biz();

/**
* Expiration time, the default is 5 seconds
* The unit is seconds
*
* @return
*/
int expireTime() default 5;

}

Processing aspects of DisLock

/**

* Processing @DisLock annotation aspect
*
* @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() String disKey = CacheKeyParser.parse(proceedingJoinPoint, disLock.key(), disLock.biz());
boolean lock = disLockUtil.lock(disKey, expireTIme);
int count = 1;
while (!lock && count lock = disLockUtil.lock(disKey, expireTIme);
count++;
TimeUnit.SECONDS.sleep(1);
}
Object proceed;
if (lock) {
// Allow query
try {
proceed = proceedingJoinPoint.proceed();
} finally {
// Delete distributed lock
disLockUtil.unlock(disKey, false);
}
} else {
throw new CustomException(ErrorCodeEnum.DUPLICATE_REQUEST.getMessage());
}
return proceed;
}

}

Redis configuration

/**

* @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. Set the configuration object of the connection pool
JedisPoolConfig config = new JedisPoolConfig();
//Set the maximum number of connections in the pool
config.setMaxTotal(50);
//Set the maximum number of connections held in the pool when idle
config.setMaxIdle(10);
config.setMaxWaitMillis(3000L);
config.setTestOnBorrow(true);
//2. Set the connection pool object
return new JedisPool(config,host,port);
}
}

Realization of redis distributed lock

/**

* Implementation of redis distributed lock
*
* @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";

/**
* Try to acquire a distributed lock
*
* @param jedis Redis client
* @param lockKey lock
* @param requestId Request ID
* @param expireTime expired time
* @return whether the acquisition is successful
*/
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;
}

/**
* Release the distributed lock
*
* @param jedis Redis client
* @param lockKey lock
* @param requestId Request ID
* @return Whether the release was successful
*/
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;
}

/**
* Release the lock
*
* @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);
// Determine whether the lock has expired
if (StringUtils.isBlank(expireTimeCache)) {
result = true;
}
if (System.currentTimeMillis()-Long.parseLong(expireTimeCache)> 0) {
// delete directly
jedis.del(key);
result = true;
}
} else {
jedis.del(key);
}
} finally {
jedis.close();
}
return result;
}

/**
* Obtain distributed lock
*
* @param key
* @param expireSecond
* @return
*/
public final boolean lock(String key, int expireSecond) {
if (StringUtils.isBlank(key)) {
throw new RuntimeException("The key passed in is empty");
}
expireSecond = expireSecond == 0? DEFAULT_EXPIRE_TIME: expireSecond;
// Time stamp when it expires
long expireTime = System.currentTimeMillis() + expireSecond * 1000 + 1;
boolean setResult = false;
Jedis jedis = jedisPool.getResource();
try {
if (jedis.setnx(key, String.valueOf(expireTime)) == 1) {
// indicates that the lock is successful
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());
// Determine whether the lock has expired
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;
}

}

The key to implementing distributed locks is to set the key. You need to obtain the actual parameters to set the distributed locks. Here, the parser is customized

/* *

* Cache key parser
*
* @author wang.js on 2019/2/27.
* @version 1.0
*/
public class CacheKeyParser {

/**
* Parse the cached key
*
* @param proceedingJoinPoint section
* @param cacheKey cache key
* @param biz business
* @return String
* @throws IllegalAccessException
*/
public static String parse(ProceedingJoinPoint proceedingJoinPoint, String cacheKey, String biz) throws IllegalAccessException {
// parse the key of the actual parameter
String key = cacheKey.replace("#", "");
StringTokenizer stringTokenizer = new StringTokenizer(key, ".");

Map 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;
}

/**
* Get parameter Map collection
*
* @param joinPoint section
* @return Map
*/
private static Map getNameAndValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues ​​= joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map param = new HashMap<>(paramNames.length);

for (int i = 0; i param.put(paramNames[i], paramValues[i]);
}
return param;
}

/**
* Get the parameter value of the specified parameter name
*
* @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)) {
//Private variables can be accessed during reflection
f.setAccessible(true);
return f.get(obj);
}
}
return null;
}

}

ErrorCodeEnum

public enum ErrorCodeEnum {


SUCCESS("Query successful", "200"),
SERVER_ERROR("Server Exception", "500"),
SECKILL_END("The spike activity has ended", "250"),
GOODS_KILLED("Successful second kill", "502"),
ERROR_SIGN("Illegal signature", "260"),
UPDATE_SUCCESS("Update successful", "0"),
SAVE_SUCCESS("Save successfully", "0"),
UPDATE_FAIL("Update failed", "256"),
EMPTY_PARAM("Parameter is empty", "257"),
SAVE_ERROR("Save failed", "262"),
SERVER_TIMEOUT("Call timeout", "501"),
USER_NOT_FOUND("User not found", "502"),
COUPON_NOT_FOUND("Coupon not found", "503"),
DUPLICATE("Duplicate", "504"),
USER_STATUS_ABNORMAL("User Status Abnormal", "505"),
NO_TOKEN("No token, please log in again", "506"),
ERROR_TOKEN("token is illegal", "507"),
EMPTY_RESULT("No data yet", "508"),
DUPLICATE_REQUEST("Duplicate Request", "509"),
;

/**
* defined message
*/
private String message;
/**
* Defined error code
*/
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;

}

Configuration file

spring:

redis:
host: mini7
port: 6379

Test

Define a method, add @RedisCache annotation, the value of cacheKey must be #actual parameter name. The format of the attribute name, if you want You can modify the parse method in CacheKeyParser into other formats

@DisLock(key = "#id", biz = CommonBizConstant.SECOND_KILL)
@Override public String testRedisCache(String id) {LOGGER.info("Call method to get value"); return "Dabo"; }

Add @ComponentScan({“com.eric”}) to the springboot startup class

< pre>/**

* @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);

}

}

Write a test class to call the above method

/**

* Basic data
*
* @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");
}
}

}

Leave a Comment

Your email address will not be published.