环境搭建

properties

1
2
3
4
5
6
7
8
9
10
11
12
server.port=1111
spring.redis.database=0
spring.redis.host=192.168.56.10
spring.redis.port=6379
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认0
spring.redis.lettuce.pool.min-idle=0

redis的相关配置。

pom.xml

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- 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>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>


<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author kylin
*/
@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();

//序列化配置
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

设置redis的key、value序列化配置。

controller

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
@RestController
public class GoodController {

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}

image-20220102171549546

创建两个redis项目,一个在端口1111运行,一个在2222运行。(redis记得启动)

image-20220102171713356

redis中给键名为goods:001设置值value为100

启动项目访问http://localhost:1111/buyGoodshttp://localhost:3333/buyGoods

image-20220102171855380

image-20220102171924317

消费商品成功~

1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/buyGoods")
public String buyGoods() {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}

image-20220102172303711

此时这段代码,在一个线程是没有问题的,但是多线程下,则会出现各种问题,所以需要加锁。

2.0(加锁)

加锁的话我们是使用synchronized还是lock呢??

image-20220102172808173

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
@RestController
public class GoodController {

@Autowired
StringRedisTemplate stringRedisTemplate;

@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() {
synchronized (this) {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}
}

如果使用synchronized,像上图这样,虽然成功加锁,但是其他的请求线程则会一直停在这等待,锁的释放,请求会一直在转圈,造成线程的挤压。

synchronizedlock的区别是在与业务。

  • 不见不散
  • 过时不候

lock可以设置尝试获取时间,超过了则做其他操作。

synchronized则一直等待。

所以我们可以使用locktryLock()方法,设置获取时间,超过了则做其他操作。

配置nginx

docker部署nginx

docker run --name nginx -p 80:80 -d nginx

image-20220102174638735

incloude /etc/nginx/conf.d/*.conf注释,否则会默认先加载这个文件下的conf配置。导致下面我们配的失效

访问http://192.168.56.10/buyGoods/则会负载均衡到本地启动的两个项目中

image-20220102180001691

此时我们访问http://192.168.56.10/buyGoods/则会轮询消费1111和3333项目了。

这样子我们这个单机版下好像解决了锁的问题(本地锁),但是分布式下是锁不住的,因为如果有10个这样的项目,每个项目同时都只有一个线程能运行,那么10个项目则会有10个线程去操作资源,这样还是多线程,会产生线程问题的!

image-20210813111419751

我们可以使用JMeter进行验证

image-20220102180153695

image-20220102180330320

点击运行之后,我们查看项目日志

image-20220102180455302

可以发现出现了多个商品,卖出去多次的情况,这样显然是不合理的!

image-20210813143732232

所以我们则需要去一个统一的地方去管理,像redis、zookeeper、mysql

为了解决这种情况,我们则需要分布式锁,选择redis,也就是redis分布式锁

3.0(redis分布式锁)

我们使用redis的set命令进行操作

image-20220102181235578

1
2
3
 public final String REDIS_LOCK = "REDIS_LOCK";  
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
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
/**
* @author kylin
*/
@RestController
public class GoodController {

public final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
StringRedisTemplate stringRedisTemplate;


@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
//如果已经存在
if (!flag) {
return "抢锁失败!";
}
synchronized (this) {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
//解锁
stringRedisTemplate.delete(REDIS_LOCK);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}

思路其实很简单,首先获取到锁的对象,会在reidis中创建一个键名为REDIS_LOCK的对象,给其设置一个随机值。随后进行操作资源,操作完成后在redis中删除该对象stringRedisTemplate.delete(REDIS_LOCK);

而后面的线程也会进行其操作通过setIfAbsent(),只有redis中没有键名为REDIS_LOCK的对象时才能设置成功,如果redis中已经存在,说明已经有线程获取到了锁,并且没有释放。设置失败则return结束。

是否还有其他问题出现呢??

加入获取锁的线程再运行中出现了异常,导致程序没有继续执行下去,从而没有把redis中的REDIS_LOCK给删除,那么后面的其他请求则都不会成功运行!

image-20220102182359065

4.0(finaly)

image-20220102182522902

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
/**
* @author kylin
*/
@RestController
public class GoodController {

public final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
StringRedisTemplate stringRedisTemplate;


@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
//如果已经存在
if (!flag) {
return "抢锁失败!";
}
synchronized (this) {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
//解锁
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}

将从redis删除对象的操作写在finally代码快中,保证最后一定能释放。

(使用的是synchronized出现异常,jvm会自动释放锁,如果使用的是Lock,则还需要在finally代码快中加入unlock操作释放锁)

是否还存在着问题呢???

上面我们假设的是程序出现异常,但是如果我们这个项目突然宕机了呢?

例如部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,就没办法保证解锁,这个key没有被删除,所以我们需要给key设置过期时间

5.0(key过期时间)

image-20220102183241190

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
/**
* @author kylin
*/
@RestController
public class GoodController {

public final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
StringRedisTemplate stringRedisTemplate;


@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
//redis设置过期时间
stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
//如果已经存在
if (!flag) {
return "抢锁失败!";
}
synchronized (this) {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
//解锁
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}

stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);

但是这样设置key+过期时间分开了,必须要合并成一行具备原子性。

否则同样创建为key,项目宕机,同样key不会删除。我们必选要保证创建key和设置key过期时间是原子操作,必须同时成功!

6.0(key原子性)

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);

image-20220102183819645

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
/**
* @author kylin
*/
@RestController
public class GoodController {

public final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
StringRedisTemplate stringRedisTemplate;


@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
//如果已经存在
if (!flag) {
return "抢锁失败!";
}
synchronized (this) {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
//解锁
stringRedisTemplate.delete(REDIS_LOCK);
}
}
}

不过还是会存在问题….

image-20220102184355346

假如A线程再设置的10秒钟内没有执行完业务,key被删除后,另一个线程B就能成功设置key,再等待A线程释放锁(等待synchronized代码快外)。A线程执行业务完成后,执行删除key,但是这个key其实不是他创建的key,是B创建的key,A创建的key已经因为到期自动删除了。

7.0(超时业务 删自己的key)

所以我们要在删key操作中做判断,判断值是否相等,从而保证在过期时间内只能自己删除自己的key。

image-20220102184912481

1
2
3
4
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
//解锁
stringRedisTemplate.delete(REDIS_LOCK);
}

不过还是有原子性的问题,if判断和删除key操作不是原子性的!

如果判断成功,程序宕机,还是不能删除掉key。所以我们要保证只要进行了value值判断,相同就一定会进行删除key的操作。

8.0(删除key原子性)

使用lua脚本

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
/**
* Redis工具类
*
* @author kylin
*/
public class RedisUtils {

private static JedisPool jedisPool;

static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);

jedisPool = new JedisPool(jedisPoolConfig, "192.168.56.10", 6379, 100000);
}

public static Jedis getJedis() throws Exception {

if (null != jedisPool) {
return jedisPool.getResource();
}
throw new Exception("Jedispool is not ok");
}
}

image-20220103173958750

不过还是有问题~

我们要确保redisLock过期时间大于业务执行时间的问题,Redis分布式锁如何续期?

还有就是Redis集群环境下,Redis是保证AP,就会出现redis异步复制造成锁的丢失。

例如:主节点没来的及把刚刚set进来的这条数据给从节点,就挂了。。

9.0(redisson)

导入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>

image-20220103183219217

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
/**
* @author kylin
*/
@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();

//序列化配置
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}

@Bean
public Redisson redisson() {

Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379").setDatabase(0);

return (Redisson) Redisson.create(config);
}
}

配置注入Redisson

image-20220103183449535

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
/**
* @author kylin
*/
@RestController
public class GoodController {

public final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Autowired
Redisson redisson;


@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}

return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
redissonLock.unlock();
}
}
}

简单方便了好多。。。还强大~~

压测请求100之后,非常和谐…

image-20220103184053412

image-20220103184004965

不过还是可能出现以上异常,也就是当前解锁线程不是锁的持有线程

9.1

image-20220103184345129

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
/**
* @author kylin
*/
@RestController
public class GoodController {

public final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
StringRedisTemplate stringRedisTemplate;

@Autowired
Redisson redisson;


@Value("${server.port}")
private String serverPort;

@GetMapping("/buyGoods")
public String buyGoods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try {
//get key 查看库存的数量够不够
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
//库存数量-1
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);

return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
}

return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
if (redissonLock.isLocked()){
if (redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
}
}
}
}
  • redissonLock.isLocked()redis是否上锁
  • redissonLock.isHeldByCurrentThread()当前线程是否是锁的持有线程