环境搭建
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
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=8
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>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.1.0</version> </dependency>
<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
|
@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() { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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; } }
|

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

redis中给键名为goods:001
设置值value为100
启动项目访问http://localhost:1111/buyGoods
和http://localhost:3333/buyGoods


消费商品成功~
1.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @GetMapping("/buyGoods") public String buyGoods() { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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; }
|

此时这段代码,在一个线程是没有问题的,但是多线程下,则会出现各种问题,所以需要加锁。
2.0(加锁)
加锁的话我们是使用synchronized
还是lock
呢??

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) { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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
,像上图这样,虽然成功加锁,但是其他的请求线程则会一直停在这等待,锁的释放,请求会一直在转圈,造成线程的挤压。
synchronized
和lock
的区别是在与业务。
lock可以设置尝试获取时间,超过了则做其他操作。
synchronized则一直等待。
所以我们可以使用lock
的tryLock()
方法,设置获取时间,超过了则做其他操作。
配置nginx
docker部署nginx
docker run --name nginx -p 80:80 -d nginx

将incloude /etc/nginx/conf.d/*.conf
注释,否则会默认先加载这个文件下的conf配置。导致下面我们配的失效
访问http://192.168.56.10/buyGoods/
则会负载均衡到本地启动的两个项目中

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

我们可以使用JMeter进行验证


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

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

所以我们则需要去一个统一的地方去管理,像redis、zookeeper、mysql
为了解决这种情况,我们则需要分布式锁,选择redis,也就是redis分布式锁
3.0(redis分布式锁)
我们使用redis的set
命令进行操作

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
|
@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) { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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
给删除,那么后面的其他请求则都不会成功运行!

4.0(finaly)

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
|
@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) { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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过期时间)

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
|
@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); stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS); if (!flag) { return "抢锁失败!"; } synchronized (this) { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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);

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
|
@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) { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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); } } }
|
不过还是会存在问题….

假如A线程再设置的10秒钟内没有执行完业务,key被删除后,另一个线程B就能成功设置key,再等待A线程释放锁(等待synchronized代码快外)。A线程执行业务完成后,执行删除key,但是这个key其实不是他创建的key,是B创建的key,A创建的key已经因为到期自动删除了。
7.0(超时业务 删自己的key)
所以我们要在删key操作中做判断,判断值是否相等,从而保证在过期时间内只能自己删除自己的key。

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
|
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"); } }
|

不过还是有问题~
我们要确保redisLock过期时间大于业务执行时间的问题,Redis分布式锁如何续期?
还有就是Redis集群环境下,Redis是保证AP
,就会出现redis异步复制造成锁的丢失。
例如:主节点没来的及把刚刚set进来的这条数据给从节点,就挂了。。
9.0(redisson)
导入依赖
1 2 3 4 5 6
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.4</version> </dependency>
|

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
|
@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

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
|
@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 { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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之后,非常和谐…


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

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
|
@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 { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0) { 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()
当前线程是否是锁的持有线程