Spring Boot & Spring Cloud 实现 redisson 分布式锁

Spring Boot 接入 redisson 分布式锁

为什么要使用分布式锁?

在分布式场景下为了保证数据最终一致性。在单进程的系统中,存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步(lock—synchronized),使其在修改这种变量时能够线性执行消除并发修改变量。但分布式系统是多部署、多进程的,开发语言提供的并发处理API在此场景下就无能为力了。

分布式锁的使用场景

电商网站用下单操作时需要使用,秒杀活动更是如此,否则会出现超卖(库存100,秒杀活动时库存变负数了)现象

分布式锁的实现方式

大概有三种:1.基于关系型数据库,2.基于缓存,3.基于zookeeper
大部分网站使用的是基于缓存的,有更好的性能,而缓存一般是以集群方式部署,保证了高可用性

基于缓存redis,使用开源 redisson 实现分布式锁

redisson: https://github.com/redisson/redisson

(1)新建Spring Boot项目,引入依赖:redisson依赖 netty、jackson-core、jackson-databind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--start redis distributed lock-->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
<!--end redis distributed lock-->

(2)在启动类中注入 redisson 的bean,可以在配置文件中添加redis的连接地址、参数,然后在代码中引入配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootApplication
public class RedissonApplication {

public static void main(String[] args) {
SpringApplication.run(RedissonApplication.class, args);
}

@Bean
Redisson redissonSentinel() {
//支持单机,主从,哨兵,集群等模式
//此为哨兵模式
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://192.168.1.1:26379")
.setPassword("123456");
return (Redisson) Redisson.create(config);
}
}

(3)到此,就完成了,是不是很简单。模拟秒杀场景,有一个商品aaa,库存100个,2个并发请求110次,每请求一次库存减1,看最后商品数有没有变为负数

(4)在redis中,设置一个库存数量为100

1
2
3
4
5
6
@Test
public void test() throws Exception {
//设置一个key,aaa商品的库存数量为100
stringRedisTemplate.opsForValue().set("aaa", "100");
Assert.assertEquals("100", stringRedisTemplate.opsForValue().get("aaa"));
}

(5)把项目复制一份,2个项目,分别使用下面的测试方法,同时启动,因库存为100,每次请求库存减1,启动2个测试方法各循环60次,总计120次请求,看库存打印结果会不会变负数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String lockKey = "testRedisson";//分布式锁的key
@Test
public void testDistributed() {
//执行的业务代码
for (int i=0; i < 60; i++) {
RLock lock = redisson.getLock(lockKey);
lock.lock(60, TimeUnit.SECONDS); //设置60秒自动释放锁(默认是30秒自动过期)
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("aaa").toString());
if (stock > 0) {
stringRedisTemplate.opsForValue().set("aaa", (stock-1)+"");
System.out.println("lockkey:" + lockKey + ",stock:" + (stock-1));
}
lock.unlock(); //释放锁
}
}

测试结果:正确,redis的值没有变负数,说明被锁住了

Spring Cloud 实现 redisson 分布式锁

需求背景

启动Redis,6379端口,Spring Cloud微服务,模拟秒杀抢购场景,100库存,用jmeter测试,起300个线程并发请求2次,总计600个请求数,最后查看库存是否为负数,证明分布式锁是否锁住了库存。

  • 注册中心 10025端口
  • 消费者服务 9700端口
  • 秒杀服务 8081、8082,启动2个服务

测试流程如下:

  1. 启动注册中心,消费者服务与秒杀服务都注册到注册中心
  2. 启动消费者服务,通过feign 以负载均衡方式调用秒杀服务
  3. 启动秒杀服务,请求秒杀服务时,修改商品库存,商品库存存储在redis中,默认100

通过jmeter执行http请求,模拟用户抢购。查看结果,从jmeter上可以看到有600个请求,有13个失败了。再看redis中的库存为0,说明锁住了,没有发生超卖现象。总共测试了多次,贴出有代表性的:

  • 1秒内启动 200线程,循环请求2次,总计400请求,成功了几十个请求,其余请求被hystrix降级返回
  • 2秒内启动 300线程,循环请求2次,总计600请求,请求全部成功,库存为0
  • 3秒内启动 500线程,循环请求2次,总计1000请求,请求全部成功,库存为0
  • 5秒内启动 1000线程,循环请求2次,总计2000请求,请求全部成功,库存为0

说明在1秒内启动200个线程并发请求,程序无法处理过来

秒杀实现

(1)秒杀服务主要依赖了redissonnetty,其他都是微服务开发需引入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>

(2)启动主类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableDiscoveryClient
@SpringBootApplication
public class DistributedLockApplication {

public static void main(String[] args) {
SpringApplication.run(DistributedLockApplication.class, args);
}

//添加redisson的bean
@Bean
public Redisson redisson() {
Config config = new Config();
//此示例是单库的,可以是主从、sentinel、集群等模式
config.useSingleServer().setAddress("redis://localhost:6379");
return (Redisson)Redisson.create(config);
}
}

(3)然后是提供秒杀功能的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RestController
public class IndexController {
private static String commodityCount = "commodityCount"; //商品key
private static String lockKey = "testRedisson"; //分布式锁的key
@Autowired private StringRedisTemplate redisTemplate;
@Autowired private Redisson redisson;

/**
* 查询是否健康
* @return
*/
@RequestMapping(value = "/health", method = RequestMethod.GET)
public String health() {
return "health";
}

/**
* 设置商品数量为100个
* @param value
* @return
*/
@RequestMapping("/setValue")
public String setValue(int value) {
redisTemplate.opsForValue().set(commodityCount, value + "");
return "success";
}

/**
* 模拟秒杀抢购,并发200个请求过来,查看是否出现超卖
* @return
*/
@RequestMapping("/spike")
public String spike() {
String flag = "success";
RLock lock = redisson.getLock(lockKey);
try {
//lock.lockAsync(5 , TimeUnit.SECONDS);
//lock.lock(5, TimeUnit.SECONDS); //设置60秒自动释放锁(默认是30秒自动过期)
Future<Boolean> res = lock.tryLockAsync(100, 5, TimeUnit.SECONDS);
boolean result = res.get();
System.out.println("result:" + result);
if (result) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get(commodityCount).toString());
if (stock > 0) {
redisTemplate.opsForValue().set(commodityCount, (stock-1)+"");
} else {
flag = "fail";
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
return flag;
}
}

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2021 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :