环境搭建
properties
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
<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
/**
* @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
@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;
}
}
创建两个redis项目,一个在端口1111运行,一个在2222运行。(redis记得启动)
redis中给键名为goods:001设置值value为100
启动项目访问http://localhost:1111/buyGoods和http://localhost:3333/buyGoods
消费商品成功~
1.0
@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;
}
此时这段代码,在一个线程是没有问题的,但是多线程下,则会出现各种问题,所以需要加锁。
2.0(加锁)
加锁的话我们是使用synchronized还是lock呢??
@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,像上图这样,虽然成功加锁,但是其他的请求线程则会一直停在这等待,锁的释放,请求会一直在转圈,造成线程的挤压。
synchronized和lock的区别是在与业务。
- 不见不散
- 过时不候
lock可以设置尝试获取时间,超过了则做其他操作。
synchronized则一直等待。
所以我们可以使用lock的tryLock()方法,设置获取时间,超过了则做其他操作。
配置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命令进行操作
public final String REDIS_LOCK = "REDIS_LOCK";
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
/**
* @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给删除,那么后面的其他请求则都不会成功运行!
4.0(finaly)
/**
* @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过期时间)
/**
* @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);
/**
* @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);
}
}
}
不过还是会存在问题....
假如A线程再设置的10秒钟内没有执行完业务,key被删除后,另一个线程B就能成功设置key,再等待A线程释放锁(等待synchronized代码快外)。A线程执行业务完成后,执行删除key,但是这个key其实不是他创建的key,是B创建的key,A创建的key已经因为到期自动删除了。
7.0(超时业务 删自己的key)
所以我们要在删key操作中做判断,判断值是否相等,从而保证在过期时间内只能自己删除自己的key。
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
//解锁
stringRedisTemplate.delete(REDIS_LOCK);
}
不过还是有原子性的问题,if判断和删除key操作不是原子性的!
如果判断成功,程序宕机,还是不能删除掉key。所以我们要保证只要进行了value值判断,相同就一定会进行删除key的操作。
8.0(删除key原子性)
使用lua脚本
/**
* 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");
}
}
不过还是有问题~
我们要确保redisLock过期时间大于业务执行时间的问题,Redis分布式锁如何续期?
还有就是Redis集群环境下,Redis是保证AP,就会出现redis异步复制造成锁的丢失。
例如:主节点没来的及把刚刚set进来的这条数据给从节点,就挂了。。
9.0(redisson)
导入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
/**
* @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
/**
* @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之后,非常和谐...
不过还是可能出现以上异常,也就是当前解锁线程不是锁的持有线程
9.1
/**
* @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()当前线程是否是锁的持有线程