先讲下流程:
1.在redis中设置一个锁setnx(lockkey , currenttime+timeout)。
2.返回1代表之前没有这个锁,那么就调用expire(lockkey)来重新设置超时时间,执行业务,接下来del(key),释放锁,最后结束。
3.返回0代表有这个锁,那么传统流程中就直接结束。优化后,调用get(lockkey)来获取这个锁的超时时间的value。
3.1接下来判断如果value不是空,并且已经超时的话,那么调用getset(lockkey,currenttime+timeout)方法,重新设置value值,同时会返回一个老的值,判断如果这个值为空,或者旧值和新值相等,那么代表这个锁没有变化,获取锁成功,调用第二步操作。
3.2如果条件不满足,那么结束任务。
Redis+Scheduled
模拟一个需求,用户提交订单后,若干时间后如果还没有支付,那么关闭这个订单。那么我们需要用定时任务做轮询,把超时的订单全部删除。
在分布式的情况下,多个程序,我只需要其中一个运行这个定时任务,不需要所有程序都做轮询,因此我们可以用到redis锁来管理。接下来写代码:
@Component
public class CloseOrderTask {
@Scheduled(cron = "0 */1 * * * ?")
public void closeOrder() throws InterruptedException {
System.out.println("关闭订单定时任务启动");
long timeout = Long.parseLong(PropertiesUtils.getProperty("lock.timeout", "5000"));
Long setnxResult = RedisShardedPoolUtil.setnx("CLOSE_ORDER_TASK_LOCK" , String.valueOf(System.currentTimeMillis() + timeout));
if (setnxResult != null && setnxResult.intValue() == 1){
//设置成功,获取锁
closeOrder("CLOSE_ORDER_TASK_LOCK");
}else {
System.out.println("没有获得锁");
}
System.out.println("关闭订单定时任务关闭");
}
private void closeOrder(String lockName) throws InterruptedException {
//设置key的有效期50秒,防止死锁
RedisShardedPoolUtil.expire(lockName , 50);
System.out.println(Thread.currentThread().getName() + "获取到锁" + lockName);
System.out.println("执行关闭订单操作");
Thread.sleep(3000);
RedisShardedPoolUtil.del(lockName);
System.out.println(Thread.currentThread().getName() + "释放锁" + lockName);
}
}
public static Long setnx(String key , String value){
ShardedJedis jedis = null;
Long result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.setnx(key , value);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnBrokenResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
每次请求都会去redis中找是否有锁,如果有的话就不做删除,如果没有的话就插入一条,并且设置超时时间。运行结果:
tomcat1:
tomcat2:
这样就实现了只有一个可以运行的目的,但是这里会有一个死锁的问题:假如程序运行到第9行,刚set进值,还没有设置超时时间的时候,程序崩掉了,那么这个锁就一直待在redis中了,那也就死锁了。
这种情况我们可以在定时任务中加一个方法:
@PreDestroy
public void delLock(){
RedisShardedPoolUtil.del("CLOSE_ORDER_TASK_LOCK");
}
@PreDestroy在程序结束时运行,但是仅限于shutdown这种平和的方式,如果kill进程,这个也是没有用的,因此我们需要演进一下代码:
@Scheduled(cron = "0 */1 * * * ?")
public void closeOrder2() throws InterruptedException {
System.out.println("关闭订单定时任务启动");
long timeout = Long.parseLong(PropertiesUtils.getProperty("lock.timeout", "5000"));
Long setnxResult = RedisShardedPoolUtil.setnx("CLOSE_ORDER_TASK_LOCK" , String.valueOf(System.currentTimeMillis() + timeout));
if (setnxResult != null && setnxResult.intValue() == 1){
//设置成功,获取锁
closeOrder("CLOSE_ORDER_TASK_LOCK");
}else {
//未获取到锁,继续判断时间戳,看是否可以重置并获取到锁
String lockValueStr = RedisShardedPoolUtil.get("CLOSE_ORDER_TASK_LOCK");
if (lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)){
String getSetResult = RedisShardedPoolUtil.getSet("CLOSE_ORDER_TASK_LOCK" , String.valueOf(System.currentTimeMillis() + timeout));
//再次用当前时间戳getset
//返回给定的key的旧值,通过判断旧值来决定是否可以获取锁
if (lockValueStr == null || (lockValueStr != null && StringUtils.equals(lockValueStr , getSetResult))){
closeOrder("CLOSE_ORDER_TASK_LOCK");
}else {
System.out.println(Thread.currentThread().getName() + "没有获得锁");
}
}else {
System.out.println(Thread.currentThread().getName() + "没有获得锁");
}
}
System.out.println("关闭订单定时任务关闭");
}
public static String getSet(String key , String value){
ShardedJedis jedis = null;
String result = null;
try {
jedis = RedisShardedPool.getJedis();
result = jedis.getSet(key , value);
} catch (Exception e) {
e.printStackTrace();
RedisShardedPool.returnBrokenResource(jedis);
return result;
}
RedisShardedPool.returnResource(jedis);
return result;
}
其实通过代码就很清楚地看出流程了,里面用了多重防死锁,这是因为在分布式环境下,程序可能不止2台,那么所有的机器同时运行大概率会有类似问题,所以我们通过多重验证来判断是否获得锁。