环境搭建
properties
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | server.port=1111spring.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
| 12
 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
| 12
 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
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | @RestControllerpublic 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
| 12
 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呢??

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | @RestControllerpublic 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命令进行操作

| 12
 3
 
 |  public final String REDIS_LOCK = "REDIS_LOCK";  String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
 Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
 
 | 
| 12
 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)

| 12
 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过期时间)

| 12
 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);

| 12
 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。

| 12
 3
 4
 
 | if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
 stringRedisTemplate.delete(REDIS_LOCK);
 }
 
 | 
不过还是有原子性的问题,if判断和删除key操作不是原子性的!
如果判断成功,程序宕机,还是不能删除掉key。所以我们要保证只要进行了value值判断,相同就一定会进行删除key的操作。
8.0(删除key原子性)
使用lua脚本
| 12
 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)
导入依赖
| 12
 3
 4
 5
 6
 
 | <dependency>
 <groupId>org.redisson</groupId>
 <artifactId>redisson</artifactId>
 <version>3.13.4</version>
 </dependency>
 
 | 

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

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

| 12
 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()当前线程是否是锁的持有线程