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

作者: ʘᴗʘ发布时间:2021-09-06 17:43 浏览量:1064 点赞:887 售价: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

其它推荐:

  • 使用Webstorm创建Vue3+tailwind css3项目

    本文介绍了如何使用webstorm快速创建一个vue3 + tailwind css3的项目,适合新手快速掌握。

  • 低成本反爬虫实战经验分享

    如何避免网站内容被爬虫恶意采集呢?本文通过介绍coderbbb一年以来和爬虫斗智斗勇的经历,启发你寻找适合自己的反爬虫方案!

  • Java Springboot使用OkHttp实现微信支付API-V3签名、证书的管理和使用

    新版的微信支付API-V3中,最让人头疼的就是各种安全措施。各种凌乱的概念让人摸不着头脑。比如微信平台证书、商户证书、API KEY等等概念。本文从零开始,引导读者一步一步实现了整个微信支付的安全验证,通过本文可以快速完成微信支付的安全开发。

  • Vditor粘贴、上传图片到阿里云OSS(WEB直传方式)

    当我们在Vditor中粘贴站外图片或直接上传本地图片的时候,我们希望图片直接上传到阿里云OSS上,不经过我们的业务服务器转发,这样可以有效降低业务服务器的带宽占用,同时还能提高图片的上传速度。本文介绍了如何配置Vditor,让其可以完美直传图片到阿里云OSS中。

  • nginx通过yum命令安装stream模块,支持TCP流量转发

    本文介绍了如何通过yum命令安装nginx,并给nginx安装stream模块,让nginx能够代理、转发TCP长连接的流量。

  • springboot logback如何关闭、禁止某个java类或jar包的日志

    日常开发中,有时候引入一些第三方的Jar包或者Java类,这些类会打印很多没用的日志,看着比较凌乱。这个时候,我们可以通过配置`logback.xml`来关闭某个java类的日志输出。

  • java通过selenium实现网页全屏截图

    本文介绍了如何使用java截图网页,通过本文介绍的方案可以实现全屏截图网页。该方案使用的是java+selenium+chrome的技术。

  • springboot使用redis限制并发请求、限流

    日常开发中经常会遇到需要限流、限制并发的需求,网上有很多算法、框架的介绍,但通常比较复杂,对于小项目来讲过于复杂。本文介绍了一种通过redis incr函数来实现的简便限流算法,并提供了完整源代码,可以快速的整合到你的项目中,实现API限流。

  • springboot整合mybatis查询mysql数据库教程

    本文介绍如何在springboot中整合mybatis来查询mysql数据库,包括各种join查询、主键自动生成、复杂resultMap映射、in查询等,通过阅读本文可以掌握springboot+mybatis的常用各种语法。

  • springboot读取jar包中Resources路径下的txt文件

    springboot读取jar包中的文件是一个常见需求,本文介绍了如何通过ClassPathResource来读取Resources路径下的txt文件。

user

ʘᴗʘ

77
文章数
73128
浏览量
57260
获赞数
67.80
总收入