Redis分布式锁
- 一、什么是分布式锁?
- 二、分布式锁的使用场景。
- 三、业务场景
- 方式一(单机部署):
- 方式二(单机部署使用线程锁 synchronized):
- 方式三(集群部署使用线程锁 synchronized):
- 方式四(集群部署使用redis锁):
- 方式五(集群部署使用redisson):
- redisson介绍
- 示例
一、什么是分布式锁?
要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
二、分布式锁的使用场景。
线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
有这样一个情境,线程A和线程B都共享某个变量X。
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。
三、业务场景
设计一个买苹果的场景,那代码该怎么写?
在redis中设置库存数量为200:
然后用JMeter去进行压测(500并发):
方式一(单机部署):
@RestController
public class Redislock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/redislock/test")
public void test() {
int appleNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("apple"));
if (appleNum > 0) {
int realAppleNum = appleNum - 1;
stringRedisTemplate.opsForValue().set("apple", realAppleNum + "");
System.out.println("扣减成功,剩余库存:" + realAppleNum);
} else {
System.out.println("扣减失败,库存不足");
}
}
}
--------------------------------------
控制台打印(截取的一小段):
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
扣减成功,剩余库存:199
结果:
根据结果可以判断该代码在高并发下是不能实现想要的功能的。
方式二(单机部署使用线程锁 synchronized):
@RestController
public class Redislock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/redislock/test")
public synchronized void test() {
int appleNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("apple"));
if (appleNum > 0) {
int realAppleNum = appleNum - 1;
stringRedisTemplate.opsForValue().set("apple", realAppleNum + "");
System.out.println("扣减成功,剩余库存:" + realAppleNum);
} else {
System.out.println("扣减失败,库存不足");
}
}
}
--------------------------------------
控制台打印(截取的一小段):
扣减成功,剩余库存:10
扣减成功,剩余库存:9
扣减成功,剩余库存:8
扣减成功,剩余库存:7
扣减成功,剩余库存:6
扣减成功,剩余库存:5
扣减成功,剩余库存:4
扣减成功,剩余库存:3
扣减成功,剩余库存:2
扣减成功,剩余库存:1
扣减成功,剩余库存:0
扣减失败,库存不足
扣减失败,库存不足
扣减失败,库存不足
扣减失败,库存不足
扣减失败,库存不足
结果:
根据结果可以判断该代码在高并发下库存逐渐减小,实现了想要的功能。但是这只是单机部署的项目,如果换成了集群部署,synchronized就没什么作用了,看下面的方式三。
方式三(集群部署使用线程锁 synchronized):
1.模拟集群部署,启动两个项目
2.nginx已部署好
3.控制台打印结果
项目1(截取的一小段):
扣减成功,剩余库存:194
扣减成功,剩余库存:193
扣减成功,剩余库存:192
扣减成功,剩余库存:191
扣减成功,剩余库存:189
扣减成功,剩余库存:188
扣减成功,剩余库存:187
扣减成功,剩余库存:186
项目2(截取的一小段):
扣减成功,剩余库存:199
扣减成功,剩余库存:198
扣减成功,剩余库存:197
扣减成功,剩余库存:196
扣减成功,剩余库存:195
扣减成功,剩余库存:194
扣减成功,剩余库存:193
扣减成功,剩余库存:192
根据结果可以看出有相同的剩余库存,则说明没有实现想要的功能,所以synchronized只是线程锁,在同一JVM中才有效果。
方式四(集群部署使用redis锁):
@RestController
public class Redislock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/redislock/test")
public String test() {
//1.设置redis锁 2.校验已存在该redis锁 3.设置redis超时时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("apple_lock", "", 10, TimeUnit.SECONDS);
while (!result) {
result = stringRedisTemplate.opsForValue().setIfAbsent("apple_lock", "", 10, TimeUnit.SECONDS);
}
try {
int appleNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("apple"));
if (appleNum > 0) {
int realAppleNum = appleNum - 1;
stringRedisTemplate.opsForValue().set("apple", realAppleNum + "");
System.out.println("扣减成功,剩余库存:" + realAppleNum);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//释放锁
stringRedisTemplate.delete("apple_lock");
}
return "success";
}
}
控制台打印:
结果:根据实验可知,redis锁实现了我们想要的功能,但其实该代码还不是太完美,忽略了一种情况:
设置超时时间是为了防止代码执行到一半系统挂掉(比如宕机了)就有可能引起redis锁没有被释放的问题,这时会引起另外一个问题,比如业务代码执行的时间过长,导致redis锁已经超时的问题。
为了解决这种问题我有两个思路:
1.定时器,重新设置超时时间
2.使用redisson的分布式锁
方式五(集群部署使用redisson):
redisson介绍
Redisson是Redis官方推荐的Java版的Redis客户端。它提供的功能非常多,也非常强大,此处我们只用它的分布式锁功能。
示例
1.pom
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.1</version>
</dependency>
2.创建bean(这里使用了redis单机模式,当然也可以配redis集群模式)
@SpringBootApplication
public class LearningApplication {
public static void main(String[] args) {
SpringApplication.run(LearningApplication.class, args);
}
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
}
3.用法
@RestController
public class Redislock {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/redislock/test")
public String test() {
RLock redissonLock = redisson.getLock("apple_lock");
try {
//如果redis锁没有释放,则会阻塞到该位置,直到redis被释放才会往下执行
redissonLock.lock(10, TimeUnit.SECONDS);
int appleNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("apple"));
if (appleNum > 0) {
int realAppleNum = appleNum - 1;
stringRedisTemplate.opsForValue().set("apple", realAppleNum + "");
System.out.println("扣减成功,剩余库存:" + realAppleNum);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//释放锁
redissonLock.unlock();
}
return "success";
}
}
4.控制台打印结果
5.结论
可以看出,只用了三行代码就可以实现方式四的所有功能
//获取锁对象
RLock redissonLock = redisson.getLock("apple_lock");
//如果redis锁没有释放,则会阻塞到该位置,直到redis锁被释放才会往下执行(不会自动续期,过期时间为10秒)
redissonLock.lock(10, TimeUnit.SECONDS);
// (不指定过期时间,同步锁默认时间为lockWatchdogTimeOut【30s】,每30/3=10s就自动续一次锁,需要业务手动释放锁)
// redissonLock.lock();
//释放锁
redissonLock.unlock();
redisson还有很多好用的api,这个可以慢慢去研究。
使用redis锁必然后会丢失一部分性能,因为redis是单线程执行,所谓的锁其实就是强制把程序设置成单线程执行,不过可以根据业务场景进行相应的改造。比如苹果有1000库存,可以把这个1000个库存分成十份,每份100个,然后设置在redis中(key:apple_001、apple_002、apple_003… value:100 100 100…),这样性能就会增加大约10倍。