前言
最近开发公司的项目,遇到了分布式的场景,即,同一条数据可能被多台服务器或者说多个线程同时修改,此时可能会出现分布式事务的问题,随即封装了redis分布式锁的注解。
场景分析
前提:我的银行卡有0元钱,现在有A,B两个人,想分别给我转10元钱
分析:
假如A,B通过读数据库,同时发现我的余额是0,这时,
线程A,会给我设置:
余额 = 10 + 0
线程B,会给我设置:
余额 = 10 + 0
最后,我的卡上收到了两个人的转账,但是最后金额居然只有10元!!这是怎么回事?
其实原因就在于多个线程,对一条数据同时进行了操作。如果我们可以设置一下,在修改的方法上面加一个锁,每次修改之前,(A)先拿到这个锁,再去做修改方法,此时,其他(B)线程想要修改的时候,看到锁已经不再,需要等待锁释放,然后再去执行,就保证了A,B先后依此执行,数据依此累加就没问题了。
解决办法
基于代码的可移植性,我将分布式锁做成了注解,大家如果有需要,可以直接将jar包拿过去做相应的修改即可,jar包下载地址(链接:https://pan.baidu.com/s/1hBn-...
提取码:1msl):
注解使用说明:
1.在需要添加分布式锁的方法上面加上@RedisLock
如果key不添加,则默认锁方法第一个参数param的id字段,如果需要指定锁某个字段,则@RedisLock(key = "code")
2.如果方法没有参数,则不可使用RedisLock锁
@RedisLock
public void updateData( Data param){
}
下面详细分析一下封装的源码:
先看一下项目结构(总共就4个类):
//RedisLock注解类:没什么好解释的
/**
* Created by liuliang on 2018/10/15.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
//被锁的数据的id
String key() default "";
//唤醒时间
long acquireTimeout() default 6000L;
//超时时间
long timeout() default 6000L;
}
//----------------------类分割线---------------------
//RedisService 一个简单的操作redis的类,封装了加锁和释放锁的方法
/**
* Created by liuliang on 2018/10/15.
*/
@Service
public class RedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Resource(name = "stringRedisTemplate")
@Autowired
ValueOperations valOpsStr;
@Autowired
RedisTemplate redisTemplate;
@Resource(name = "redisTemplate")
ValueOperations valOpsObj;
public String getStr(String key) {
return stringRedisTemplate.opsForValue().get(key);//获取对应key的value
// return valOpsStr.get(key);
}
public void setStr(String key, String val) {
stringRedisTemplate.opsForValue().set(key,val,1800, TimeUnit.SECONDS);
// valOpsStr.set(key, val);
}
public void del(String key) {
stringRedisTemplate.delete(key);
}
/**
* 根据指定o获取Object
*
* @param o
* @return
*/
public Object getObj(Object o) {
return valOpsObj.get(o);
}
/**
* * 设置obj缓存
* * @param o1
* * @param o2
*
*/
public void setObj(Object o1, Object o2) {
valOpsObj.set(o1, o2);
}
/**
* 删除Obj缓存
*
* @param o
*/
public void delObj(Object o) {
redisTemplate.delete(o);
}
private static JedisPool pool = null;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 设置最大连接数
config.setMaxTotal(200);
// 设置最大空闲数
config.setMaxIdle(8);
// 设置最大等待时间
config.setMaxWaitMillis(1000 * 100);
// 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
DistributedLock lock = new DistributedLock(pool);
/**
* redis分布式加锁
* @param objectId
* @param acquireTimeout
* @param timeout
*/
public String redisLock(String objectId,Long acquireTimeout, Long timeout) {
// 对key为id加锁, 返回锁的value值,供释放锁时候进行判断
String lockValue = lock.lockWithTimeout(objectId, acquireTimeout, timeout);
System.out.println(Thread.currentThread().getName() + "获得了锁");
return lockValue;
}
/**
* 释放redis分布式锁
* @param objectId
* @param lockValue
*/
public Boolean releaseLock(String objectId,String lockValue){
boolean b = lock.releaseLock(objectId, lockValue);
System.out.println(Thread.currentThread().getName() + "释放了锁");
return b;
}
//----------------------类分割线---------------------
/**
* Created by liuliang on 2018/10/15.
*
* 分布式锁的主要类,主要方法就是加锁和释放锁
*具体的逻辑在代码注释里面写的很清楚了
*/
@Slf4j
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加锁
* @param locaName 锁的key
* @param acquireTimeout 获取超时时间
* @param timeout 锁的超时时间
* @return 锁标识
*/
public String lockWithTimeout(String locaName,
long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 获取连接
conn = jedisPool.getResource();
// 随机生成一个value
String identifier = UUID.randomUUID().toString();
// 锁名,即key值
String lockKey = "lock:" + locaName;
// 超时时间,上锁后超过此时间则自动释放锁
int lockExpire = (int)(timeout / 1000);
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
log.info("lock...lock...");
if (conn.setnx(lockKey, identifier) == 1) {
log.info("==============lock success!=============");
conn.expire(lockKey, lockExpire);
// 返回value值,用于释放锁时间确认
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key没有设置超时时间,为key设置一个超时时间
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
//这里sleep 10ms是为了防止线程饥饿,各位可以思考一下为什么
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 释放锁
* @param lockName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 监视lock,准备开始事务
conn.watch(lockKey);
//避免空指针
String lockKeyValue = conn.get(lockKey)==null?"":conn.get(lockKey);
// 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
if (lockKeyValue.equals(identifier)) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List results = transaction.exec();
if (results == null) {
continue;
}
log.info("==============unlock success!=============");
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
//----------------------类分割线---------------------
/**
* Created by liuliang on 2018/10/16.
这是一个拦截器,我们指定拦截RedisLock注解
*/
@Aspect
@Component
@Slf4j
public class RedisLockAop {
ThreadLocal beginTime = new ThreadLocal<>();
ThreadLocal objectId = new ThreadLocal<>();
ThreadLocal lockValue = new ThreadLocal<>();
@Autowired
private RedisService redisService;
@Pointcut("@annotation(redisLock)")
public void serviceStatistics(RedisLock redisLock) {
}
@Before("serviceStatistics(redisLock)")
public void doBefore(JoinPoint joinPoint, RedisLock redisLock) {
// 记录请求到达时间
beginTime.set(System.currentTimeMillis());
//注解所在方法名
String methodName = joinPoint.getSignature().getName();
//注解所在类
String className = joinPoint.getSignature().getDeclaringTypeName();
//方法上的参数
Object[] args = joinPoint.getArgs();
String key = redisLock.key();
if(ObjectUtils.isNullOrEmpty(args)){
//方法的参数是空,生成永远不重复的uuid,相当于不做控制
key = methodName + UUID.randomUUID().toString();
}else {
//取第一个参数指定字段,若没有指定,则取id字段
Object arg = args[0];
log.info("arg:"+arg.toString());
Map map = getKeyAndValue(arg);
Object o = map.get(StringUtils.isEmpty(key) ? "id" : key);
if(ObjectUtils.isNullOrEmpty(o)){
//自定义异常,可以换成自己项目的异常
throw new MallException(RespCode.REDIS_LOCK_KEY_NULL);
}
key = o.toString();
}
log.info("线程:"+Thread.currentThread().getName() + ", 已进入方法:"+className+"."+methodName);
// objectId.set(StringUtils.isEmpty(redisLock.key()) ? UserUtils.getCurrentUser().getId() : redisLock.key());
objectId.set(key);
String lock = redisService.redisLock(objectId.get(), redisLock.acquireTimeout(), redisLock.timeout());
lockValue.set(lock);
log.info("objectId:"+objectId.get()+",lockValue:"+lock +",已经加锁!");
}
@After("serviceStatistics(redisLock)")
public void doAfter(JoinPoint joinPoint,RedisLock redisLock) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getSignature().getDeclaringTypeName();
redisService.releaseLock(objectId.get(),lockValue.get());
log.info("objectId:"+objectId.get()+",lockValue:"+lockValue.get() +",已经解锁!");
log.info("线程:"+Thread.currentThread().getName() + ", 已退出方法:"+className+"."+methodName+",耗时:"+(System.currentTimeMillis() - beginTime.get() +" 毫秒!"));
}
//这是一个Object转mapd的方法
public static Map getKeyAndValue(Object obj) {
Map map = new HashMap();
// 得到类对象
Class userCla = (Class) obj.getClass();
/* 得到类中的所有属性集合 */
Field[] fs = userCla.getDeclaredFields();
for (int i = 0; i < fs.length; i++) {
Field f = fs[i];
f.setAccessible(true); // 设置些属性是可以访问的
Object val = new Object();
try {
val = f.get(obj);
// 得到此属性的值
map.put(f.getName(), val);// 设置键值
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
/*
* String type = f.getType().toString();//得到此属性的类型 if
* (type.endsWith("String")) {
* System.out.println(f.getType()+"\t是String"); f.set(obj,"12") ;
* //给属性设值 }else if(type.endsWith("int") ||
* type.endsWith("Integer")){
* System.out.println(f.getType()+"\t是int"); f.set(obj,12) ; //给属性设值
* }else{ System.out.println(f.getType()+"\t"); }
*/
}
System.out.println("单个对象的所有键值==反射==" + map.toString());
return map;
}
}