Redis搭配RedisTemplate实现分布式锁实战实例

一、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;
    }
}

  • 17
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RedisTemplate是Spring Data Redis提供的一个Redis客户端操作工具类,它可以方便地实现分布式锁实现分布式锁的基本思路是:在Redis中创建一个key,作为的标识,然后在获取的时候,使用Redis的setnx命令来设置这个key的值,如果设置成功,则表示获取到了,否则表示已经被其他线程占用。 具体实现步骤如下: 1. 创建一个RedisTemplate对象,用于操作Redis。 2. 定义一个方法,用于获取,方法中使用Redis的setnx命令来设置key的值,如果设置成功,则表示获取到了,否则表示已经被其他线程占用。 3. 定义一个方法,用于释放,方法中使用Redis的del命令来删除key。 示例代码如下: ``` public class RedisLock { private RedisTemplate redisTemplate; public RedisLock(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 获取 * * @param key 的标识 * @param expire 的过期时间,单位为秒 * @return true表示获取成功,false表示获取失败 */ public boolean lock(String key, int expire) { Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "locked"); if (result != null && result) { redisTemplate.expire(key, expire, TimeUnit.SECONDS); return true; } return false; } /** * 释放 * * @param key 的标识 */ public void unlock(String key) { redisTemplate.delete(key); } } ``` 使用示例: ``` // 创建RedisTemplate对象 RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(new JedisConnectionFactory()); // 创建RedisLock对象 RedisLock redisLock = new RedisLock(redisTemplate); // 获取 boolean locked = redisLock.lock("test_lock", 60); if (locked) { // 执行业务逻辑 // ... // 释放 redisLock.unlock("test_lock"); } else { // 获取失败 // ... } ``` 以上就是使用RedisTemplate实现分布式锁的方法。需要注意的是,获取和释放的操作必须是原子性的,否则会出现死或者失效的情况。因此,在实现分布式锁的时候,需要考虑到线程安全的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值