Redisson实现分布式锁完整实例 多线程模拟并发(单机)

1 前言

随着分布式系统以及spring cloud等微服务架构的普及,对于分布式锁的掌握成为了每个程序员必须掌握的基操。常见的分布式锁的实现方法有基于数据库,基于分布式协调系统,基于缓存三种。本文通过Redisson 分布式重入锁用法,来简单实现分布式锁。

加锁逻辑:
根据给定key判断锁存不存在

  • 如果锁不存在则新增锁,并设置重入计数为1,并设置过期时间。
  • 如果锁存在,且唯一标识匹配,则表明锁重入请求,重入计数+1,并设置过期时间。
  • 如果锁存在,但唯一标识不匹配,则表明被其他线程占用,返回剩余过期时间。当前线程自旋等待。

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,本文将以单点模式为例。

2 环境及准备

2.1 环境

OS:win10
Redis: 3.0.504(windows版)
Database: Mysql 8.0.22(InnoDB)
Framework:Spring Boot + JPA
IDE: STS4

2.2 准备

  • 需要本地已经安装Mysql数据库,并创建redission_test数据库。启动项目由hibernate自动创建表后,插入id为1的测试数据。
  • 需保证本地redis服务器正常启动

友情链接——windows版redis:https://github.com/microsoftarchive/redis
在这里插入图片描述

3 编码

先上一张项目结构图。
项目结构图

文件命用途
RedissonTestApplication.java项目入口
RedissonConfig.javaRedission配置类(读取redission-single.yml)
RedissonTestController.java接受请求接口(设置mapping)
RedissonTestControllerImpl.java接受请求实现类(调用service)
Goods.javaPOJO类
GoodsRepository.java仓库接口(类似于Mybatis的Mapper)
RedissonTestService.java服务接口
RedissonTestServiceImpl.java服务实现类(减小商品库存)
application.yml项目配置文件
redisson-single.ymlRedission配置文件

3.1 引入maven依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.alibaba</groupId>
	<artifactId>Redisson-test</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>RedissonTest</name>
	<description>Demo project for redission</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.validation</groupId>
			<artifactId>validation-api</artifactId>
			<!-- <version>2.1.0.Final</version> -->
		</dependency>
		<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.10.6</version>
        </dependency>
        <dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

3.2 配置数据源,redis地址,JPA设置

这里需要将数据库的账号密码改成你自己的。Redis的密码也需要修改,如果没有密码请将password注释掉。

server:
  port: 8080
spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/redission_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone = GMT&allowPublicKeyRetrieval=true
    username: yourusername
    password: yourpassword
  redis:
    #Redis数据库索引(默认为0)
    port: 6379
    #Redis服务器地址
    host: 127.0.0.1
    #Redis数据库索引(默认为0)
    database: 0
    #Redis服务器连接密码(默认为空,如无请注释掉password否则可能报错)
    password: yourpassword
    # 连接池最大连接数(使用负值表示没有限制)
    jedis:
      pool:
        max-active: 8
        #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        #连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0
    #连接超时时间(毫秒)
    timeout: 5000ms
  jpa:
    #自动生成表
    generate-ddl: true
    open-in-view: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
        ddl-auto: update

3.3 创建redisson-single.yml用于配置单机版Redis

同样注意要将password修改成你的密码,如果没有密码这里改成null,同clientName。

singleServerConfig: 
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  password: yourpassword
  subscriptionsPerConnection: 5
  clientName: null
  address: "redis://127.0.0.1:6379"
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  database: 0
  #dnsMonitoring: false
  dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"

3.4 创建RedissonConfig配置类,用redisson-single.yml来配置Redission

package com.alibaba.test.config;

import java.io.IOException;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@Configuration
public class RedissonConfig {
	
	@Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        RedissonClient redisson = Redisson.create(
                Config.fromYAML(new ClassPathResource("redisson-single.yml").getInputStream()));
        return redisson;
    }

}

3.5 创建POJO类,这里为商品

表名为goods,有商品id,商品名,库存数量三个字段。Hibernate会根据这个类自动生成表。

package com.alibaba.test.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
@Table( name = "goods" )
public class Goods {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@NotBlank
	@Size( max = 20 )
	private String goodsName;
	
	@NotBlank
	private Integer goodsStock;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getGoodsName() {
		return goodsName;
	}

	public void setGoodsName(String goodsName) {
		this.goodsName = goodsName;
	}

	public Integer getGoodsStock() {
		return goodsStock;
	}

	public void setGoodsStock(Integer goodsStock) {
		this.goodsStock = goodsStock;
	}

}

3.6 Repostiory接口

类似与Mybatis的Mapper接口,但是JPA提供了findAll,getById等许多默认操作,也可以在该接口中自定义hql。而Mybatis需要在mapper.xml中定义sql。

package com.alibaba.test.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.alibaba.test.entity.Goods;

@Repository
public interface GoodsRepository extends JpaRepository<Goods, Long>{

}

3.7 编写接受请求接口(Controller)

可以用PostMan或Advanced REST client等工具通过url(http://localhost:8080/api/buy)访问该接口

package com.alibaba.test.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api")
public interface RedissonTestController {

	@RequestMapping("/buy")
	public ResponseEntity<?> buy();
	
}

3.8 Controller的实现类

这里会注入service,通过调用service减少库存。这里创建了一个线程池,并建了1000个线程提交给线程池执行,以此来模拟一千个用户同时购买goodsId为1的商品。(但由于核心线程池设了16,其实并发数只有16,这里可以设成1000)

package com.alibaba.test.controller.impl;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

import com.alibaba.test.controller.RedissonTestController;
import com.alibaba.test.service.RedissonTestService;

@Component
public class RedissonTestControllerImpl implements RedissonTestController{

	@Autowired
	RedissonTestService redissonTestService;
	
	@Override
	public ResponseEntity<?> buy() {
		// TODO Auto-generated method stub
		final Long goodsId = 1L;
		
		int corePoolSize = 16;
		int maximumPoolSize = 50;
		long KeepAliveTime = 2;
		TimeUnit unit = TimeUnit.SECONDS;
		BlockingQueue workQueue = new LinkedBlockingQueue<>(1000);
		//ThreadFactory threadFactory,
		RejectedExecutionHandler handler = new AbortPolicy();
		ThreadPoolExecutor customThreadPoolExecutor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,KeepAliveTime,unit,workQueue);
		
		for( int i=0; i< 1000; i++) {
			
			customThreadPoolExecutor.execute( new BuySimulationTask(goodsId) );

		}
		
		return null;
	}

	class BuySimulationTask extends Thread{
		
		private Long goodsId;

	    public BuySimulationTask(Long goodsId) {
	        this.goodsId = goodsId;
	    }
		
		public void run() {
			redissonTestService.decreasetProductStock( goodsId );
		}
	}
}

3.9 服务接口

package com.alibaba.test.service;

import org.springframework.stereotype.Service;

@Service
public interface RedissonTestService{

	public boolean decreasetProductStock(Long goodsId);
	
}

3.10 服务实现类(减小商品库存)

这里用了两种方法实现同步,第一种是同步关键字synchronized,此方法同ReentryLock加锁类似,只针对单JVM应用有效。而下面一种则使用了Redisson的加锁解锁方法来实现同步。

package com.alibaba.test.service.impl;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.alibaba.test.entity.Goods;
import com.alibaba.test.repository.GoodsRepository;
import com.alibaba.test.service.RedissonTestService;

@Component
public class RedissonTestServiceImpl implements RedissonTestService{

	@Autowired
	private GoodsRepository goodsRepository;
	
	@Autowired
	private RedissonClient redissonClient;
	
//	@Override
//	synchronized public boolean decreasetProductStock(Long goodsId) {
//		// TODO Auto-generated method stub
//		Goods good = goodsRepository.findById(goodsId)
//				.orElseThrow( () -> new RuntimeException("Goods Not Found with id") );
//		if( good.getGoodsStock() > 0 ) {
//			good.setGoodsStock( good.getGoodsStock() - 1 );
//		}else {
//			return false;
//		}
//		goodsRepository.save(good);
//		return true;
//	}
	
	@Override
	public boolean decreasetProductStock(Long goodsId) {
		// TODO Auto-generated method stub
		RLock lock = redissonClient.getLock(String.valueOf(goodsId));
		try {
	        //加锁
	        lock.lock();
			Goods good = goodsRepository.findById(goodsId)
					.orElseThrow( () -> new RuntimeException("Goods Not Found with id") );
			if( good.getGoodsStock() > 0 ) {
				good.setGoodsStock( good.getGoodsStock() - 1 );
			}else {
				return false;
			}
			goodsRepository.save(good);
		}
		catch( Exception e ) {
			System.out.println(e.getMessage());
		}finally {
			//解锁
			lock.unlock();
		}
		return true;
	}

}

4 测试

启动项目
在这里插入图片描述

4.1 不加锁的情况

执行完会发现商品库存没有被正确修改。

	@Override
	public boolean decreasetProductStock(Long goodsId) {
		// TODO Auto-generated method stub
		Goods good = goodsRepository.findById(goodsId)
				.orElseThrow( () -> new RuntimeException("Goods Not Found with id") );
		if( good.getGoodsStock() > 0 ) {
			good.setGoodsStock( good.getGoodsStock() - 1 );
		}else {
			return false;
		}
		goodsRepository.save(good);
		return true;
	}

执行前:库存1000
在这里插入图片描述

调用接口(http://localhost:8080/api/buy):
在这里插入图片描述

执行后:应该减去1000,实际没有

在这里插入图片描述

4.2 加synchronized的情况

修改代码和数据库里的库存值。可以看到方法被同步执行,单本方法只对单jvm应用有效。在分布式系统下无效。

	@Override
	synchronized public boolean decreasetProductStock(Long goodsId) {
		// TODO Auto-generated method stub
		Goods good = goodsRepository.findById(goodsId)
				.orElseThrow( () -> new RuntimeException("Goods Not Found with id") );
		if( good.getGoodsStock() > 0 ) {
			good.setGoodsStock( good.getGoodsStock() - 1 );
		}else {
			return false;
		}
		goodsRepository.save(good);
		return true;
	}

执行前:库存1000
在这里插入图片描述

执行后:减去1000,归0。注意不是一下子归0的,注意查询全部线程结束后的值。
在这里插入图片描述

4.3 运用Redisson的lock和unlock方法

执行效果同上,但针对分布式系统有效。

执行前:库存1000
在这里插入图片描述

执行后:减去1000,归0。注意不是一下子归0的,注意查询全部线程结束后的值。
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redisson是一个基于Redis实现的Java驻内存数据网格(In-Memory Data Grid)和分布式锁(Distributed Lock)框架,它提供了一系列的分布式数据结构,其中包括分布式锁实现使用Redisson实现分布式锁非常简单,只需要遵循以下步骤: 1. 引入Redisson的依赖包: ```xml <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.12.6</version> </dependency> ``` 2. 创建Redisson客户端对象: ```java Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); ``` 3. 获取锁对象: ```java RLock lock = redisson.getLock("myLock"); ``` 4. 加锁: ```java lock.lock(); ``` 5. 执行业务逻辑: ```java try { // 执行业务逻辑 } finally { // 释放锁 lock.unlock(); } ``` 完整的示例代码如下: ```java public class MyService { private RedissonClient redisson; public MyService() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); redisson = Redisson.create(config); } public void myMethod() { RLock lock = redisson.getLock("myLock"); try { lock.lock(); // 执行业务逻辑 } finally { lock.unlock(); } } } ``` 需要注意的是,在执行业务逻辑的过程中,一定要放在try...finally块中,并在finally块中释放锁,以确保在出现异常时锁能够正确地被释放。 ### 回答2: Redisson是一种基于Redis实现分布式锁框架。以下是Redisson分布式锁使用说明: 1. 引入Redisson依赖:首先需要在项目中引入Redisson的依赖,可以通过Maven或者Gradle等构建工具来管理依赖。 2. 创建RedissonClient:使用RedissonClient可以连接到Redis服务器,并获取一个分布式锁对象。 3. 加锁:使用分布式锁对象可以通过lock()方法来获取锁,该方法默认的锁超时时间是30秒,超过该时间锁会自动释放。也可以使用自定义的锁超时时间。 4. 解锁:在加锁的代码块执行完毕后,需要调用unlock()方法手动释放锁,确保锁的释放,避免死锁的产生。 5. 锁的可重入性:Redisson分布式锁支持可重入性,即同一个线程可以多次获取同一个锁,在释放锁的时候需要调用相应次数的unlock()方法来释放锁。 6. 锁的异步执行:Redisson分布式锁也支持异步执行,即lock()方法可以通过异步方式获取锁。 7. 锁的公平性:Redisson分布式锁可以选择是否公平锁,默认为非公平锁,即不保障获取锁的顺序。可以通过配置参数来设置公平锁。 8. 锁监控:Redisson提供了监控分布式锁的功能,可以通过调用getLock("/lock")方法来监控名为"lock"的锁的情况。 总结来说,Redisson分布式锁使用非常简单,只需要引入依赖、创建客户端、加锁和解锁即可。同时,Redisson还提供了可重入性、异步执行、公平锁和锁监控等功能,可以根据实际需求进行配置和使用。通过使用Redisson分布式锁,可以有效地控制多线程环境下共享资源的访问,避免数据不一致和竞态条件的发生。 ### 回答3: Redisson是一个基于Redis的Java实现,提供了一系列分布式相关的功能,其中包括分布式锁使用使用Redisson实现分布式锁,首先需要创建一个RedissonClient实例,通过该实例可以获取一个RLock对象,RLock提供了加锁和释放锁的方法。 在加锁方面,Redisson支持公平锁和非公平锁。公平锁会按照请求的顺序依次加锁,而非公平锁则允许插队,谁先抢到锁就谁先执行。通过调用RLock对象的lock()方法可以获取锁,该方法会一直阻塞直到获取到锁为止。如果不希望一直阻塞,可以使用tryLock()方法,该方法会尝试获取锁一段时间,如果超过指定的等待时间仍未获取到锁,则返回false。 在释放锁方面,可以使用unlock()方法来释放锁,只有加锁方才能释放锁。可以通过判断当前线程是否持有锁,再决定是否释放锁。 使用Redisson分布式锁还有一些其他的特性,比如锁的自动续约、可重入锁、可中断锁等,这些特性都可以通过相应的方法来使用。 总的来说,Redisson分布式锁使用起来非常方便,只需要简单的几行代码就可以实现分布式环境下的锁功能。但需要注意的是,在使用分布式锁时要考虑并发情况、死锁问题以及锁的粒度等,以确保代码的正确性和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值