Spring Boot 集成 Redisson 实现分布式锁



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 推荐参考资料

Redisson 官方 Github

Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)

Redis 分布式 客户端 Redisson 分布式锁快速入门

6 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值