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 && countlock = 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, ".");
MapnameAndValue = 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 MapgetNameAndValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Mapparam = new HashMap<>(paramNames.length);
for (int i = 0; iparam.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 && countlock = 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, ".");
MapnameAndValue = 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 MapgetNameAndValue(ProceedingJoinPoint joinPoint) {
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Mapparam = new HashMap<>(paramNames.length);
for (int i = 0; iparam.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");
}
}
}