Redis实现高并发分布式锁

分布式锁场景

在分布式环境下多个操作需要以原子的方式执行

首先启一个springboot项目,再引入redis依赖包:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

以下是一个扣减库存的接口作为例子:

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
		if (stock > 0) {
			int realStock = stock - 1;
			stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
			System.out.println(扣减成功,剩余库存:" + realStock + "");
		} else {
			System.out.println(扣减失败,库存不足!" );
		}
		return "end";
	}
}
1.单实例应用场景

以上代码使用JMeter压测工具进行调用,设置参数为:

  1. Number Of Threads[users]:100
  2. Ramp Up Period[in seconds]:0
  3. Loop Count:2

用单个web调用,结果出现并发问题:
在这里插入图片描述
解决方案:加入同步锁(synchronized)

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		synchronized(this) {
			int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
			if (stock > 0) {
				int realStock = stock - 1;
				stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
				System.out.println(扣减成功,剩余库存:" + realStock + "");
			} else {
				System.out.println(扣减失败,库存不足!" );
			}
			return "end"; 
		}
	}
}
2.多实例分布式场景

以上代码,比如有多个应用程序,用nginx做负载均衡,进行同时调用压测
两个程序存在同样的扣减,出现并发现象。
第一个应用扣减结果显示:
在这里插入图片描述
第二个应用扣减结果显示:
在这里插入图片描述
解决方案:redis的setnx方法(可参考SETNX的api)
多个线程setnx调用时,有且仅有一个线程会拿到这把锁,所以拿到锁的执行业务代码,最后释放掉锁,代码如下:

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		String lockkey = "lockkey";
		Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx
		if(!result) {
			return "";
		}
		int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
		if (stock > 0) {
			int realStock = stock - 1;
			stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
			System.out.println(扣减成功,剩余库存:" + realStock + "");
		} else {
			System.out.println(扣减失败,库存不足!" );
		}
		springRedisTemplate.delete(lockkey);
		return "end"; 
	}
}

调用200次,压测结果显示还是有问题,只减掉了一部分:
在这里插入图片描述
这时,加大压测次数,结果正常了:
第一个应用扣减结果显示:
在这里插入图片描述
第二个应用扣减结果显示:
在这里插入图片描述
这个只是因为加大了调用次数,执行业务代码需要一点时间,这段时间拒绝了很多等待获取锁的请求。但是,还是有问题,假如redis服务挂掉了,抛出异常了,这时锁不会被释放掉,出现死锁问题,可以添加try catch处理,代码如下:

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		String lockkey = "lockkey";
		try{
			Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx
			if(!result) {
				return "";
			}
			int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
			if (stock > 0) {
				int realStock = stock - 1;
				stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
				System.out.println(扣减成功,剩余库存:" + realStock + "");
			} else {
				System.out.println(扣减失败,库存不足!" );
			}
		}finally{
			springRedisTemplate.delete(lockkey);
		}
		return "end"; 
	}
}

这时,Redis服务挂掉导致死锁的问题解决了,但是,如果服务器果宕机了,又会导致锁不能被释放的现象,所以可以设置超时时间为10s,代码如下:

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		String lockkey = "lockkey";
		try{
			Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue",10,TimeUnit.SECONDS);//jedis.setnx
			//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx
			//stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);
			if(!result) {
				return "";
			}
			int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
			if (stock > 0) {
				int realStock = stock - 1;
				stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
				System.out.println(扣减成功,剩余库存:" + realStock + "");
			} else {
				System.out.println(扣减失败,库存不足!" );
			}
		}finally{
			springRedisTemplate.delete(lockkey);
		}
		return "end"; 
	}
}

这时,如果有一个线程执行需要15s,当执行到10s时第二个线程进来拿到这把锁,会出现多个线程拿到同一把锁执行,在第一个线程执行完时会释放掉第二个线程的锁,以此类推…就会导致锁的永久失效。所以,只能自己释放自己的锁,可以给当前线程取一个名字,代码如下:

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		String lockkey = "lockkey";
		String clientId = UUID.randomUUID().toString();
		try{
			Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,clientId ,10,TimeUnit.SECONDS);//jedis.setnx
			//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx
			//stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);
			if(!result) {
				return "";
			}
			int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
			if (stock > 0) {
				int realStock = stock - 1;
				stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
				System.out.println(扣减成功,剩余库存:" + realStock + "");
			} else {
				System.out.println(扣减失败,库存不足!" );
			}
		}finally{
			springRedisTemplate.delete(lockkey);
		}
		return "end"; 
	}
}

永久失效的问题解决了,但是,如果第一个线程执行15s,还是会存在多个线程拥有同一把锁的现象。所以,需要续期超时时间,当一个线程执行5s后对超时时间进行续期都10s,就可以解决了,续期设置可以借助redission工具。

Redission使用

Redission分布式锁实现原理:
在这里插入图片描述
pom.xml

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.6.5</version>
</dependency>

Application.java启动类

@bean
public Redission redission {
	//此为单机模式
	Config config = new Config();
	config.useSingleServer().setAddress("redis://120.0.0.1:6379").setDatabase(0);
	return (Redission)Redission.creat(config);
}

最终解决以上所有问题的代码如下:

@RestController
public class IndexController {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;
	@Autowired
	private Redissionredission;

	@RequestMapping("/deduct_stock")
	public Stirng deductStock() {
		String lockkey = "lockkey";
		//String clientId = UUID.randomUUID().toString();
		RLock lock = redission.getLock();
		try{
			//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,clientId ,10,TimeUnit.SECONDS);//jedis.setnx
			//Boolean result = stringRedisTemplate.opsForValue.setIfAbsent(lockkey,"lockvalue");//jedis.setnx
			//stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);
			//加锁:redission默认超时时间为30s,每10s续期一次,也可以自己设置时间
			lock.lock(60,TimeUnit.SECONDS);
			int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get(key)
			if (stock > 0) {
				int realStock = stock - 1;
				stringRedisTemplate.opsForValue.set("stock",realStock+"");//jedis.set(key,value)
				System.out.println(扣减成功,剩余库存:" + realStock + "");
			} else {
				System.out.println(扣减失败,库存不足!" );
			}
		}finally{
			lock.unlock();
			//springRedisTemplate.delete(lockkey);
		}
		return "end"; 
	}
}

高并发分布式锁的问题得到解决。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值