文章目录
1 摘要
在分布式系统中高并发带来的除了流量大之外,更容易出现的是重复写操作的问题,典型的场景如抢票、购物等,一件商品只能卖一次,如果高并发的情况下,则容易出现一件商品被卖多次的情况。为避免重复写操作的问题,则需要通过加锁的方式保证写操作依次执行。常用的分布式锁实现方式有 Redis 和 Zookeeper。本文将介绍使用 Spring Boot 集成 Redission 实现分布式锁功能。Spring boot 默认的 Redis 客户端为 Lettuse,使用 Redisson 不会和 Jedis 或者 Lettuce 发生冲突,Redisson 作为第三方依赖可独立配置。
Redisson 官方 Github:https://github.com/redisson/redisson
2 核心 Maven 依赖
./demo-base-web/pom.xml
<!-- redisson 分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
其中 ${redisson.version}
的版本信息为 <redisson.version>3.16.3</redisson.version>
3 核心代码
3.1 Redisson 配置类
./demo-base-web/src/main/java/com/ljq/demo/springboot/baseweb/config/RedissonConfig.java
package com.ljq.demo.springboot.baseweb.config;
import lombok.Getter;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: Redisson 分布式锁配置类
* @Author: junqiang.lu
* @Date: 2021/9/27
*/
@Getter
@Configuration
public class RedissonConfig {
/**
* redis 地址
*/
@Value(value = "${redisson.address}")
private String address;
/**
* redis 数据库编号
*/
@Value(value = "${redisson.database}")
private Integer database;
/**
* redis 密码
*/
@Value(value = "${redisson.password}")
private String password;
/**
* redis 最小连接数量
*/
@Value(value = "${redisson.connectionMinimumIdleSize}")
private Integer connectionMinimumIdleSize;
/**
* redis 最大连接池大小
*/
@Value(value = "${redisson.connectionPoolSize}")
private Integer connectionPoolSize;
/**
* redis 连接超时时间(毫秒)
*/
@Value(value = "${redisson.connectionTimeout}")
private Integer connectionTimeout;
/**
* redis 服务器响应时间(毫秒)
*/
@Value(value = "${redisson.timeout}")
private Integer timeout;
/**
* 创建 Redisson 客户端
*
* @return
*/
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
/**
* 运行模式
* useSingleServer: 单机模式
* useMasterSlaveServers: 主从模式
* useSentinelServers: 哨兵模式
*/
config.useSingleServer()
.setAddress(address)
.setDatabase(database)
.setPassword(password)
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
.setConnectionPoolSize(connectionPoolSize)
.setConnectTimeout(connectionTimeout)
.setTimeout(timeout);
return Redisson.create(config);
}
}
3.2 Redisson 配置文件(yml)
./demo-web/src/main/resources/application-dev.yml
./demo-web/src/main/resources/application-test.yml
./demo-web/src/main/resources/application-pro.yml
## Redisson 分布式锁配置
redisson:
address: redis://172.16.140.10:7749
database: 9
password: 21cde022-a5da-626e-c8f4-04c99ed3a181
connectionMinimumIdleSize: 24
connectionPoolSize: 64
connectionTimeout: 10000
timeout: 3000
3.3 使用示例
./demo-service/src/main/java/com/ljq/demo/springboot/service/impl/RestUserServiceImpl.java
package com.ljq.demo.springboot.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.ljq.demo.springboot.baseweb.api.ApiResult;
import com.ljq.demo.springboot.dao.restuser.RestUserDao;
import com.ljq.demo.springboot.entity.RestUserEntity;
import com.ljq.demo.springboot.service.RestUserService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* REST示例-用户表业务层具体实现类
*
* @author junqiang.lu
* @date 2019-09-19 17:19:42
*/
@Service("restUserService")
@Transactional(rollbackFor = {Exception.class})
@Slf4j
public class RestUserServiceImpl implements RestUserService {
@Autowired
private RestUserDao restUserDao;
@Autowired
private RedissonClient redissonClient;
/**
* 分布式锁测试
*
* @return
*/
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public ApiResult distributedLock() {
RestUserEntity restUser = new RestUserEntity();
restUser.setUserName("张三");
restUser.setEmail("tomcat@163.com");
restUser.setId(1L);
restUser.setUserStatus(1);
restUser.setPasscode("123456");
String lockKey = "REDIS_LOCK_KEY_DEMO";
RLock lock = redissonClient.getLock(lockKey);
boolean tryLock = false;
try {
tryLock = lock.tryLock(30L, 180L, TimeUnit.SECONDS);
if (tryLock) {
RestUserEntity restUserDB = restUserDao.queryObject(restUser);
log.info("获取到锁了,线程名称: {}, 线程 id: {}", Thread.currentThread().getName(),
Thread.currentThread().getId());
if (Objects.isNull(restUserDB)) {
restUserDao.save(restUser);
} else {
BeanUtil.copyProperties(restUser, restUserDB, CopyOptions.create().ignoreNullValue());
restUserDao.update(restUserDB);
}
}
} catch (InterruptedException e) {
log.error("线程阻塞", e);
} finally {
if (lock.isLocked()) {
log.info("线程主动释放锁");
lock.unlock();
}
}
return ApiResult.success();
}
}
4 验证锁是否生效
主要思想: 启动两个(或多个)相同的服务,使用压力测试工具并发请求接口
实际测试操作:
(1)数据库准备
注意: 创建数据库后需要保证 ID 为 1 的数据不存在,这样才能够在第一次查询后进行创建操作。在并发情况下,会出现多个线程同时查询到数据为空,然后执行插入操作。如果没有锁,则会出现多条记录;有锁则只会有一条插入记录,其他线程均为更新操作。
./doc/sql/rest_user_create.sql
/*==============================================================*/
/* DBMS name: MySQL 5.0 */
/* Created on: 2021/9/29 14:59:22 */
/*==============================================================*/
drop table if exists rest_user;
/*==============================================================*/
/* Table: rest_user */
/*==============================================================*/
create table rest_user
(
id bigint(20) unsigned not null auto_increment comment 'id 主键',
user_name varchar(30) default null comment '用户名',
passcode varchar(100) default null comment '登陆密码',
email varchar(50) default null comment '邮箱',
user_status tinyint default 1 comment '用户账号状态,1正常(默认),2禁止登陆',
insert_time datetime default current_timestamp comment '用户注册时间',
insert_operator_id bigint comment '创建用户 id',
insert_identity tinyint comment '创建人身份标识',
update_time datetime default current_timestamp on update current_timestamp comment '用户更新时间',
update_operator_id bigint comment '更新人用户 id',
update_identity tinyint comment '更新人身份标识',
versions int unsigned default 1 comment '版本控制字段(默认1)',
del_sign tinyint default 0 comment '逻辑删除字段,0正常(默认),1删除',
primary key (id)
)
engine=innodb default charset=utf8mb4 comment='REST用户';
(2) 多个相同的服务
编写两个 Springboot 启动类,使用不同的服务端口
./demo-web/src/main/java/com/ljq/demo/springboot/web/DemoWebApplication.java
./demo-web/src/main/java/com/ljq/demo/springboot/web/DemoWebApplication2.java
package com.ljq.demo.springboot.web;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.annotation.ComponentScan;
/**
* @author Caleb
* @date 2018-10-09
*/
@SpringBootApplication
@EnableEurekaServer
@ComponentScan(basePackages = {"com.ljq.demo"})
@MapperScan("com.ljq.demo.springboot.dao")
@ServletComponentScan
@EnableCaching
public class DemoWebApplication2 extends SpringBootServletInitializer {
public static void main(String[] args) {
System.setProperty("server.port", "8089");
SpringApplication.run(DemoWebApplication2.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(DemoWebApplication2.class);
}
}
(3) 一套针对数据库表的增删改查
(4) 删掉分布式锁相关代码,同时重新建表,作对比测试
5 推荐参考资料
Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)
Redis 分布式 客户端 Redisson 分布式锁快速入门
6 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.