基于Redis实现分布式锁&附带详细测试案例

▌前提摘要

    本文基于redis数据库

           -- redis 192.168.1.211:6379

redis安装:https://blog.csdn.net/qq_37936542/article/details/78522728

springboot集成redis:https://blog.csdn.net/qq_37936542/article/details/80104308
 

 

▌准备工作

导入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
 
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>

application.yml配置redis

spring: 
  redis: 
    database: 0
    host: 192.168.1.211
    port: 6379
    password: 123
    timeout: 3000
    jedis: 
      pool: 
        max-active: 8
        max-idle: 500
        min-idle: 0

配置JedisPool

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class JedisConfig {

	@Value("${spring.redis.host}")
	private String host;

	@Value("${spring.redis.port}")
	private int port;

	@Value("${spring.redis.jedis.pool.max-active}")
	private int maxTotal;

	@Value("${spring.redis.jedis.pool.max-idle}")
	private int maxIdle;

	@Value("${spring.redis.jedis.pool.min-idle}")
	private int minIdle;

	@Value("${spring.redis.password}")
	private String password;

	@Value("${spring.redis.timeout}")
	private int timeout;

	@Bean
	public JedisPool redisPoolFactory() {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxTotal(maxTotal);
		jedisPoolConfig.setMaxIdle(maxIdle);
		jedisPoolConfig.setMinIdle(minIdle);
		JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
		return jedisPool;
	}
}

 

▌ redis实现分布式锁流程图

 

1:redis通过while(true)循环尝试去拿锁,只要拿锁失败继续尝试

2:等到拿锁成功以后,去执行相关的业务逻辑代码

3:业务逻辑执行完毕,最后将锁给释放

 

▌ redis实现分布式锁的注意事项

1:为了防止程序崩溃导致死锁产生,生成key的时候需要设置合理的过期时间,就算程序崩溃了也可以自动释放锁

2:解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,通过判断value值来实现

 

▌ 代码实现

InterfaceLock:定义获取锁&释放锁的方法接口

import redis.clients.jedis.Jedis;

public interface InterfaceLock {

	// 获取锁
	public void lock(Jedis jedis);

	// 释放锁
	public void unLock(Jedis jedis);

}

DistributeLock:实现分布式锁

import java.util.Collections;
import java.util.UUID;
import redis.clients.jedis.Jedis;

public class DistributeLock implements InterfaceLock {

	// 定义锁的key值
	private String key = "lockKey";

	// 定义锁key的自动过期时间
	private long expireTime = 10000;
	
	// 定义value
	private String identity = UUID.randomUUID().toString();

	/**
	 * 加锁操作
	 */
	public void lock(Jedis jedis) {
		// 持续去尝试拿锁
		while (true) {
			// 一旦拿到锁,结束循环
			if (tryLock(jedis)) 
				return;
			
		}
	}
	
	/**
	 * 尝试加锁
	 */
	public boolean tryLock(Jedis jedis) {
		// set方法加锁
		String result = jedis.set(key, identity, "NX", "PX", expireTime);
		// 加锁成功返回true,反之返回false
		if ("OK".equals(result))
			return true;
		return false;
	}

	/**
	 * 释放锁,redis+lua实现
	 */
	public void unLock(Jedis jedis) {
		// lua脚本语义:根据传入的key获取value,如果value等于传入的identity则删除key
		// 保证了加锁和解锁是同一个客户端
		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		// 执行lua脚本
		jedis.eval(script, Collections.singletonList(key), Collections.singletonList(identity));
	}

}

jedis.set()方法详解:

key:定义的锁key

value:随机生成的uuid,用于判断加锁和解锁操作是否是同一个客户端

nxxx:只能设置NX或者XX,如果设置NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set

expx:只能设置EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒

time:key过期时间

 

▌ 案例图解

不加锁的抢购代码:SedKillController(两个服务器的代码一致)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.service.RedisService;

@RestController
public class SedKillController {

	@Autowired
	private RedisService redisService;

	// 定义商品key值
	private String key = "goods";

	@GetMapping("sedkill")
	public String sedKill() {

		// 获取商品数量
		Object obj = redisService.get(key);

		int mount = (int) obj;

		// 如果商品被抢完,直接返回
		if (mount < 0 || mount == 0) {
			return "很遗憾,商品已被抢完";
		}

		// 线程睡眠,目的在于放大错误
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		// 抢到商品后,将redis的商品数量减一
		redisService.set(key, --mount);

		// 打印,以便观察
		System.out.println(Thread.currentThread().getName() + ":抢到第" + (mount + 1) + "件商品");

		return "恭喜,商品抢购成功";

	}
}

首先往redis放10件商品

启动8080、8081服务,打开浏览器请求两个服务器的接口进行测试,观察打印结果

8080打印:

8081打印:

完犊子,这都是些啥,下面我们使用常用的synchronized锁试试效果,修改sedkill方法

在重新测试,查看打印结果,不要忘记将redis的商品数据重置为10奥

8080打印:

8081打印:

 咦,有惊喜,比之前打印结果好看多了,至少单服务器不会出现抢购同一商品的情况了,但是还是有问题,两台服务器竟然可以抢到同一件商品,这是绝对不允许的,下面我们尝试加上刚写好的分布式锁尝试一下
 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mote.lock.DistributeLock;
import com.mote.service.RedisService;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@RestController
public class SedKillController {

	@Autowired
	private RedisService redisService;

	private String key = "goods";

	@Autowired
	private JedisPool jedisPool;

	@GetMapping("sedkill")
	public String sedKill() {

		// 获取分布式锁对象
		DistributeLock lock = new DistributeLock();

		// 获取jedis
		Jedis jedis = jedisPool.getResource();

		try {
			// 上锁
			lock.lock(jedis);

			// 获取商品数量
			Object obj = redisService.get(key);

			int mount = (int) obj;

			// 如果商品被抢完,直接返回
			if (mount < 0 || mount == 0) {
				// 解锁(不要忘了这里解锁)
				lock.unLock(jedis);
				return "很遗憾,商品已被抢完";
			}

			// 线程睡眠,目的在于放大错误

			Thread.sleep(2000);

			// 抢到商品后,将redis的商品数量减一
			redisService.set(key, --mount);

			// 打印,以便观察
			System.out.println(Thread.currentThread().getName() + ":抢到第" + (mount + 1) + "件商品");

			// 解锁
			lock.unLock(jedis);

			return "恭喜,商品抢购成功";
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			// 关闭jedis连接
			jedis.close();
		}
		return "很遗憾,商品已被抢完";

	}
}

再走一遍,开启服务器,访问接口,查看打印,不要忘记将redis商品重新置为10

8080打印:

8081打印:

完美打印,符合预期

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值