当前位置:   article > 正文

Java随笔 | Springboot项目使用redis、redisson实现分布式锁(单锁、联锁)_java 联锁

java 联锁


一、分布式锁的概念

1.1 进程、线程、锁

  • 进程
    进程是一个在内存中运行的应用程序。
    比如在Windows系统中,一个运行的xx.exe就是一个进程。
  • 线程
    线程是进程中的一个执行任务。
    例如在一个Java程序中,查询数据库的数据就是一个线程。
  • 进程与线程的关系
    进程是操作系统资源分配的基本单位,线程是处理器任务调度和执行的基本单位。
    一个进程可以有多个线程,但至少有一个线程。

  • 当多个线程操作同一份资源时,为保证数据的准确性,需要确保在同一时间内只有一个线程操作资源。
    为了实现这一目的,需要在某个地方做个标记,线程发现存在标记则等待,直到拥有标记的线程执行完毕取消标记,等待线程再去设置标记。这个标记可以理解为锁。

1.2 单进程锁、分布式单锁

  • 单进程锁
    如果一份资源会被多个线程操作,且这些线程只存在于一个进程中,此时设置的锁便是单进程锁。
  • 分布式锁
    如果一份资源会被多个线程操作,但这些线程存在于不同进程中(典型的就是分布式系统),此时设置的锁便是分布式锁。
    例如:对商品库存数据的修改,可能存在很多用户同时下单扣库存的情况,这些扣库存的线程不在同一个进程,就需要使用分布式锁。

1.3 分布式联锁

分布式单锁解决的是多个线程操作同一个资源时冲突的问题。
然而,在有些场景下,例如一个用户的购物车中多件商品的库存更改(多个资源),这些资源作为一个整体,需要保证同时被锁住,全部完成修改才可释放锁,此时就需要使用分布式联锁。

二、使用Redis实现分布式单锁

2.1 使用命令

使用redis原生命令加锁
SET key value EX seconds NX
其中:
key 锁名,通常命名为:业务模块:资源id,例如 book:stock:1 表示操作的资源为 id为1的图书对象的库存,这样命名能保证线程之间冲突(冲突才能被锁住)
value 加锁者,通常为 ip:线程号 用于标注加锁的线程
EX seconds 加锁时长(秒) ,可选,设置加锁时长,到时无论线程是否完成资源操作,均会释放锁,防止死锁的发生。
NX 该参数能实现效果:只有当不存在该key的锁时才会新增key,否则无法新增。如果没有NX参数,已存在同名key时,SET命令做的是修改操作,该参数也是实现锁的关键。
解锁
使用redis内置的lua脚本,先判断加锁者是否是当前解锁者,若是则删除key,若否则抛异常回滚。
因为redis执行lua脚本是阻塞的,这样可保证操作的原子性。

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

2.2 Springboot中编码实现

  • 在pom.xml中引入redis依赖
<dependencies>
    ...
    <!--版本全局指定-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    ...
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 在application.yml中配置redis
spring:
  redis:
    host: xxx.xxx.xxx.xxx  # ip
    port: 6379  # 默认端口
    password: ****** # 密码 没有则不写
  • 1
  • 2
  • 3
  • 4
  • 5
  • 编码
    注:以下编码中的解锁仅示意逻辑,实际需要使用Lua解锁,保证操作的原子性。
@Autowired
private StringRedisTemplate template;// 引入redis
private static final String LOCK = "sum:mod:1";// 锁名
private static final Integer SECS = 5;// 过期时间 秒 防止死锁

public void run(){
    ValueOperations<String, String> operations = template.opsForValue();// 获取操作redis的接口对象operations
    String name = Thread.currentThread().getName();// 线程名
    // 上锁,不存在则新增并返回true 否则返回false 参数:锁名 线程名 有效期 时间粒度
    Boolean success = operations.setIfAbsent(LOCK, name, SECS, TimeUnit.SECONDS);
    // 判断是否拿到锁 拿到则执行 否则不执行
    if (Boolean.TRUE.equals(success)) {
        // 主业务代码...
        if(operations.get(LOCK)==null) throw new RuntimeException(name+"锁已超时释放(rollback)");
        else if (!name.equals(operations.get(LOCK))) throw new RuntimeException("加锁解锁线程不匹配");
        template.delete(LOCK);// 执行完毕 释放锁
    } else {
        System.out.println(name+"没有拿到锁,继续等待");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

三、使用Redisson实现分布式锁

3.1 单锁的实现

  • 在pom.xml中引入redisson依赖,引入该依赖后不用再单独引入redis依赖。
<dependencies>
    ...
    <!--用redisson替代spring-boot-redis-->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>{指定版本 例如3.19.1}</version>
    </dependency>
    ....
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • application.yml配置文件中关于redis的配置不用更改,参考上文。编码实现分布式单锁:
@Resource
private RedissonClient redisson;

// 执行添加操作
public void add(Integer id, Integer num){
	...
    RLock lock = redisson.getLock("sum:mod:" + id);// 生成锁 参数为锁名
    try {
        lock.lock(5, TimeUnit.SECONDS);// 加锁 设置5秒过期
        // 主业务代码...
    } finally {
        lock.unlock();// 解锁
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.2 联锁的实现

@Resource
private RedissonClient redisson;
private static final String LOCK="book:stock:";// 锁名 根据业务需要取名

// 传入的参数为要同时锁住的资源个数
public void demo(Integer lockNum) {
    ...
    // 生成分布式锁
    RLock[] locks = new RLock[lockNum];
    for (int i=0; i< lockNum; i++)
        locks[i] = redisson.getLock(LOCK + i);
    RLock multiLock = redisson.getMultiLock(locks);// 生成联锁
    try {
        multiLock.lock(5, TimeUnit.SECONDS);// 加锁 设置5秒过期
     	// 主业务代码,操作多个资源,例如使用for循环...
    }finally {
        multiLock.unlock();// 解锁
    }
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/852839
推荐阅读
相关标签
  

闽ICP备14008679号