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.java | Redission配置类(读取redission-single.yml) |
RedissonTestController.java | 接受请求接口(设置mapping) |
RedissonTestControllerImpl.java | 接受请求实现类(调用service) |
Goods.java | POJO类 |
GoodsRepository.java | 仓库接口(类似于Mybatis的Mapper) |
RedissonTestService.java | 服务接口 |
RedissonTestServiceImpl.java | 服务实现类(减小商品库存) |
application.yml | 项目配置文件 |
redisson-single.yml | Redission配置文件 |
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的,注意查询全部线程结束后的值。