一、SETNX

 

在Redis中一般会使用setnx命令实现分布式锁。当使用setnx 命令设置一个kv时
如:
setnx lockKey lockValue

当lockKey存在时,会返回1时表示设置成功。

当lockKey不存在时,会返回0时表示设置值失败

根据以上理论就可以实现redis的分布式锁:

加锁

第一版:只使用setnx,这种方式的缺点是容易产生死锁,因为有可能忘记解锁,或者解锁失败。

setnx key value
第二版:给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
setnx key value
expire key seconds
第三版:通过 “set...nx...” 命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
set key value nx ex seconds

解锁

解锁就是删除代表锁的那份数据。
del key

问题:虽然看起来已经很完美了,但实际上还有隐患,如下图。进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,所以此时它释放的可能是其他线程的锁。

想要解决这个问题,我们需要解决两件事情:
(1) 在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。可以为key 赋一个随机值,来充当进程的标识。
(2)解锁时要先判断、再释放,这两步需要保证原子性,否则第二步失败的话,就会出现死锁。而获取和删除命令不是原子的,这就需要采用Lua 脚本,通过 Lua 脚本将两个命令编排在一起,而整个Lua脚本的执行过程是原子的。
按照以上思路最终方案如下:

加锁

set key random-value nx ex seconds

解锁

if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else
    return 0 end

二、使用RedisTemplate实现

有了以上理论基础,再通过Spring框架提供的RedisTemplate我们就可以轻松的封装出自己的RedisLock类

/**
 * Redis 分布式锁
 *
 **/
@Component
public class RedisLockUtils {

    @Autowired
    private RedisTemplate redisTemplate;

    //分布式锁过期时间 s  可以根据业务自己调节
    private static final Long LOCK_REDIS_TIMEOUT = 10L;
    //分布式锁休眠 至 再次尝试获取 的等待时间 ms 可以根据业务自己调节
    public static final Long LOCK_REDIS_WAIT = 500L;


    /**
     *  加锁
     **/
    public Boolean getLock(String key,String value){
        Boolean lockStatus = this.redisTemplate.opsForValue().setIfAbsent(key,value, Duration.ofSeconds(LOCK_REDIS_TIMEOUT));
        return lockStatus;
    }

    /**
     *  释放锁
     **/
    public Long releaseLock(String key,String value){
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript,Long.class);
        Long releaseStatus = (Long)this.redisTemplate.execute(redisScript, Collections.singletonList(key),value);
        return releaseStatus;
    }
}

调用例子:

@Service
public class SysUserServiceImpl implements ISysUserService
{
    private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);

    @Autowired
    private RedisLockUtils redisLockUtils;

    @Override
    public Boolean demo1(Double money) throws InterruptedException {
        String key = "test:key";
        String value = IdUtils.randomUUID();
        //redis尝试获取锁,加锁
        Boolean getLock = this.redisLockUtils.getLock(key,value);
        if(getLock){
            log.info("{}:成功获取[{}]锁",Thread.currentThread().getName(),key);
            //业务开始
            SysUser addMoenyPO = new SysUser();
            Long userId = SecurityUtils.getUserId();
            addMoenyPO.setUserId(userId);
            addMoenyPO.setMoney(money);
            this.userMapper.toUpMoney(addMoenyPO);
            //业务结束
            //释放分布式锁
            this.redisLockUtils.releaseLock(key,value);
            log.info("{}:释放[{}]锁",Thread.currentThread().getName(),key);
        }else{
            //线程休眠 然后尝试递归获取锁
            log.info("{}:尝试获取[{}]锁",Thread.currentThread().getName(),key);
            Thread.sleep(RedisLockUtils.LOCK_REDIS_WAIT);
            this.topUp(money);
        }
        return true;
    }
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐