Springboot使用redis的setnx和getset实现并发锁、分布式锁

作者: ʘᴗʘ发布时间:2021-09-06 17:43 浏览量:785 点赞:664 售价:0

为什么需要分布式锁

在日常开发中,很多业务场景必须保证原子性。举几个例子:

  1. 支付订单的操作,就不允许同一个订单,被同时支付,否则会产生错误的数据。
  2. 拍卖一套房子的下单操作,商品只有一个,那只能一个人下单成功。

如果你只有一台服务器,只运行一个Java程序,那么可以使用Java语言自身的一些锁来实现原子性。但如果我们有多台服务器,甚至不同服务器上跑的是不同的语言。那这时候,我们就需要一个跨平台、跨语言的加锁方式。redis就是其中最方便的一种。

核心操作和原理

使用redis实现并发锁,主要是靠两个redis的命令:setnx和getset。

  • setnx的作用是,当一个key不存在的时候,给它赋值。如果key存在或赋值失败,都会返回错误。
  • getset的作用是,先获取一个key的值,然后再给这个key赋新的值,该命令有原子性。

那我们的设计思路就是:

  1. 先用setnx初步获取锁(set当前的时间戳),如果取不到,那么有两种可能,要么是锁被其他线程持有,要么是其他线程使用完锁后,没有正确释放。
  2. 所以这个时候,我们需要验证这个锁是否过期。就是把setnx的值拿出来(一个时间戳),和当前时间戳求差,看看超时没有(超时时间是自己设定的)
  3. 如果锁超时了,我们需要释放它,让它能重新工作。但第二步的操作,不是原子性的。可能有多个线程发现这个锁过期了,都想释放它。这时候,就需要getset这个原子操作,来保证只有一个线程成功。

核心代码

@Service
public class RedisLockService {

    @Autowired
    private RedisService redisService;

    /**
     * 获取一个Redis分布式锁
     * @param lockKey    锁的Key,全局不可重复
     * @param lockExpire 锁超时时间,单位毫秒
     * @return
     */
    public boolean getLock(String lockKey, long lockExpire) {
        String redisKey = BaseCommonConfig.REDIS_LOCK_KEY + lockKey;
        if (!redisService.setnx(redisKey, String.valueOf(System.currentTimeMillis()))) {
            //没有拿到锁,但有可能是上一个加锁的人忘了释放锁。所以下面验证锁是否超时。
            String lockString = redisService.getString(redisKey);
            if (lockString == null) {
                //前面setnx时,该值还存在,现在不存在了。要么是自然过期了,要么是被别人删掉,准备重新加锁了。稳妥起见,这里返回false
                return false;
            }
            long timestamp = Long.parseLong(lockString);
            if (System.currentTimeMillis() - timestamp > lockExpire) {
                //锁已经超时
                //先get值,再set值。原子操作,确保不会多个线程进入后面的逻辑
                String oldTimestamp = redisService.setGet(redisKey, String.valueOf(System.currentTimeMillis()));
                if (oldTimestamp != null && oldTimestamp.equals(lockString)) {
                    //如果get不到值,或者get到的值不是前面取出来那个了,说明这个锁已经被别的线程占用了。

                    //第二次锁竞争成功
                    redisService.setExpireMills(redisKey, lockExpire);
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }
        redisService.setExpireMills(redisKey, lockExpire);
        return true;
    }

    /**
     * 删除锁,释放锁
     *
     * @param lockKey
     */
    public void delLock(String lockKey) {
        String redisKey = BaseCommonConfig.REDIS_LOCK_KEY + lockKey;
        redisService.del(redisKey);
    }
}

上面的代码使用了一个RedisService的类,里面主要是简单封装了一下redis的操作,你可以替换为自己的service。代码如下:

@Service
public class RedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    public Boolean setnx(String key, String value) {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        return ops.setIfAbsent(key, value);
    }

    public String setGet(String key, String value) {
        return stringRedisTemplate.opsForValue().getAndSet(key, value);
    }


    public Long incr(String key) {
        return stringRedisTemplate.opsForValue().increment(key);
    }

    public Long incr(String key,long val) {
        return stringRedisTemplate.opsForValue().increment(key,val);
    }

    public Long decr(String key) {
        return stringRedisTemplate.opsForValue().decrement(key);
    }

    public Long decr(String key,long val) {
        return stringRedisTemplate.opsForValue().decrement(key,val);
    }

    public Long setPutString(String key, String value) {
        return stringRedisTemplate.opsForSet().add(key, value);
    }

    public Boolean setExist(String key, String member) {
        return stringRedisTemplate.opsForSet().isMember(key, member);
    }

    public Set<String> setList(String key) {
        return stringRedisTemplate.opsForSet().members(key);
    }

    public Long setSize(String key) {
        return stringRedisTemplate.opsForSet().size(key);
    }

    /**
     * 逐渐废弃没有过期时间的Redis put操作
     * @param key
     * @param value
     */
    @Deprecated
    public void putString(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    /**
     * s为单位。
     *
     * @param key
     * @param value
     * @param time
     */
    public void putString(String key, String value, long time) {
        stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    public Boolean zPutString(String key, String value, long time) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
    }

    public void putString(String key, String value, long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, time, unit);
    }

    public String getString(String key) {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        return ops.get(key);
    }

    public void putObject(String key, Object value, long time, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, time, unit);
    }

    public Object getObject(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public Boolean exist(String key) {
        return stringRedisTemplate.hasKey(key);
    }

    /**
     * key有效时间
     *
     * @param key
     * @return
     */
    public Long expire(String key) {
        return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    public Boolean changeExpire(String key, long time) {
        return stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    public Boolean del(String key) {
        return stringRedisTemplate.delete(key);
    }

    public void hset(String key, String field, String value) {
        HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
        ops.put(key, field, value);
    }

    public String hget(String key, String field) {
        HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
        return ops.get(key, field);
    }

    public Map<String, String> hmget(String key) {
        HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
        return ops.entries(key);
    }

    public void hmset(String key, HashMap<String,String> data) {
        HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
        ops.putAll(key,data);
    }

    public void hdel(String key, String field) {
        HashOperations<String, String, String> ops = stringRedisTemplate.opsForHash();
        ops.delete(key, field);
    }

    public void sadd(String key, String value) {
        stringRedisTemplate.opsForSet().add(key, value);
    }

    public Boolean setIsMember(String key,String val){
        return stringRedisTemplate.opsForSet().isMember(key,val);
    }

    public Long ttl(String key) {
        return stringRedisTemplate.getExpire(key);
    }

    public void setExpire(String key, long time) {
        stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    public void setExpireMills(String key, long time) {
        stringRedisTemplate.expire(key, time, TimeUnit.MILLISECONDS);
    }

    public List<String> mget(List<String> keys) {
        return stringRedisTemplate.opsForValue().multiGet(keys);
    }

}

交个朋友

以上代码有任何疑问,可以点击右侧边栏联系作者。收费5毛~交个朋友,欢迎来撩!

版权声明:《Springboot使用redis的setnx和getset实现并发锁、分布式锁》为CoderBBB作者「ʘᴗʘ」的原创文章,转载请附上原文出处链接及本声明。

原文链接:https://www.coderbbb.com/articles/2

其它推荐:

user

ʘᴗʘ

77
文章数
51156
浏览量
40866
获赞数
67.80
总收入