1、导入pom.xml
org.springframework.boot
spring-boot-starter-data-redis
org.redisson
redisson-spring-boot-starter
3.15.6
2、redis和redisson配置
#redis配置
redis:
host: 127.0.0.1
port: 6379
database: 0
# redis连接超时时间
timeout: 10s
package com.lezu.springboot.configuration.redis;
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
// @Bean
// public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
// RedisTemplate template = new RedisTemplate();
// template.setConnectionFactory(factory);
// // key采用String的序列化方式
// template.setKeySerializer(new StringRedisSerializer());
// // hash的key也采用String的序列化方式
// template.setHashKeySerializer(new StringRedisSerializer());
// // value序列化方式采用jackson
// template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// // hash的value序列化方式采用jackson
// template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// template.afterPropertiesSet();
// return template;
// }
@Bean
public Redisson redisson() {
//单机模式
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return (Redisson) Redisson.create(config);
}
// 主从
// Config config = new Config();
// config.useMasterSlaveServers()
// .setMasterAddress("127.0.0.1:6379")
// .addSlaveAddress("127.0.0.1:6389", "127.0.0.1:6332", "127.0.0.1:6419")
// .addSlaveAddress("127.0.0.1:6399");
// RedissonClient redisson = Redisson.create(config);
//哨兵
// Config config = new Config();
// config.useSentinelServers()
// .setMasterName("mymaster")
// .addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379")
// .addSentinelAddress("127.0.0.1:26319");
// RedissonClient redisson = Redisson.create(config);
//集群
// Config config = new Config();
// config.useClusterServers()
// .setScanInterval(2000) // cluster state scan interval in milliseconds
// .addNodeAddress("127.0.0.1:7000", "127.0.0.1:7001")
// .addNodeAddress("127.0.0.1:7002");
// RedissonClient redisson = Redisson.create(config);
}
3、redis分布式锁工具类
package com.lezu.springboot.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 10,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
/**
* 加锁
*
* @param key
* @param userId
* @param timeOut
* @return
*/
public boolean lock(String key, String userId, long timeOut) {
boolean success = redisTemplate.opsForValue().setIfAbsent(key, userId, timeOut, TimeUnit.SECONDS);
if (!success) {
log.error("key:{},userId:{} get lock failer ", key, userId);
return false;
}
try {
renewExpiration(key, userId, timeOut);
TimeUnit.SECONDS.sleep(2);
return true;
} catch (Exception e) {
log.error("lock occur error:{}", e);
return false;
} finally {
releaseLock(key, userId);
}
}
/**
* 续命操作
*
* @param key
* @param timeOut
*/
private void renewExpiration(String key, String userId, long timeOut) {
threadPool.execute(() -> {
Long currentTimeMills = System.currentTimeMillis();
System.out.println("开始:" + LocalDateTime.now() + " ,timeOut:" + timeOut);
while (true) {
if (redisTemplate.hasKey(key) && userId.equals(redisTemplate.opsForValue().get(key))) {
Long cost = System.currentTimeMillis() - currentTimeMills;
//过期时间还剩余一半进行续命
if (cost > timeOut * 1000 / 2) {
currentTimeMills = System.currentTimeMillis();
redisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
System.out.println("进行中:" + LocalDateTime.now() + " ,timeOut:" + timeOut);
}
} else {
break;
}
}
});
}
/**
* 释放锁
*
* @param key
* @param userId
*/
public void releaseLock(String key, String userId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript redisScript = new DefaultRedisScript(script, Long.class);
Long result2 = redisTemplate.execute(redisScript, Arrays.asList(key), userId);
System.out.println("result2=" + result2);
}
}
4、实际使用操作
package com.lezu.springboot.controller;
import com.lezu.springboot.configuration.ServerConfig;
import com.lezu.springboot.utils.RedisLockUtil;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RestController
public class RedisController {
@Autowired
private RedisLockUtil releaseLock;
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ServerConfig serverConfig;
/**
* 使用ReentrantLock锁解决单体应用的并发问题
*/
private Lock lock = new ReentrantLock();
/**
* 单机业务
*
* @return
*/
@RequestMapping("/deductStock1")
public String deductStock1() throws InterruptedException {
/**
* 单机下单操作
* 存在的问题:
* 并发量过大会存在超卖问题
*/
//商品数量
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int resultStock = stock - 1;
redisTemplate.opsForValue().set("stock", resultStock + "");
System.out.println("扣减成功,剩余库存:" + resultStock + "---当前IP和端口号地址:" + serverConfig.getUrl());
} else {
System.out.println("扣减失败,库存不足!");
}
return "success";
}
/**
* 使用synchronized来进行加锁操作(解决并发量过大发生超卖或重复卖的问题)
* 存在的问题:
* 分布式多台机器应用下会存在超卖
*/
@RequestMapping("/deductStock2")
public String deductStock2() throws InterruptedException {
synchronized (this) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int resultStock = stock - 1;
redisTemplate.opsForValue().set("stock", resultStock + "");
System.out.println("扣减成功,剩余库存:" + resultStock + "---当前IP和端口号地址:" + serverConfig.getUrl());
} else {
System.out.println("扣减失败,库存不足!");
}
}
return "success";
}
/**
* 使用ReentrantLock加锁
* 存在的问题:
* 分布式多台机器应用下会存在超卖
*/
@RequestMapping("/deductStock3")
public String deductStock3() throws InterruptedException {
lock.lock();
try {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int resultStock = stock - 1;
redisTemplate.opsForValue().set("stock", resultStock + "");
System.out.println("扣减成功,剩余库存:" + resultStock + "---当前IP和端口号地址:" + serverConfig.getUrl());
} else {
System.out.println("扣减失败,库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
return "error";
} finally {
lock.unlock();
}
return "success";
}
/**
* 分布式锁示例
* 存在的问题
*
* @return
*/
@RequestMapping("/deductStock4")
public String deductStock4() {
String lockKey = "product_001"; //商品ID(前端传入)
try {
/**
* 存在的问题
* 1、突然宕机.... finally释放不了锁
* 2、设置了超时时间 代码报错 设置超时时间的代码没有走
*/
// Boolean result = redisTemplate.opsForValue().setIfAbsent("product_001", "userId");
// //业务代码....
//
// //代码突然报错导致设置超时时间这段代码没走
// redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
/**
* 存在的问题
* 1、加锁的这个key最多存活10秒钟,如果程序执行了10秒中该线程还想持有这把锁就会存在问题(不管怎么设置超时时间都不合理)
*/
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "lock",10, TimeUnit.SECONDS);
//加锁失败直接返回error
if (!result) {
return "系统繁忙,请稍后再试!";
}
//执行业务代码...
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int resultStock = stock - 1;
redisTemplate.opsForValue().set("stock", resultStock + "");
System.out.println("扣减成功,剩余库存:" + resultStock + "---当前IP和端口号地址:" + serverConfig.getUrl());
} else {
System.out.println("扣减失败,库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
return "error";
} finally {
//删除key释放锁
redisTemplate.delete(lockKey);
}
return "success";
}
/**
* 加锁并且设置超时时间
* 自己写一个续命线程来防止超时时间的不合理操作
* 存在的问题:
* 并发量过大会存在加锁失败和超卖的问题
*
* @return
*/
@RequestMapping("/deductStock5")
public String deductStock5() {
String lockKey = "product_001"; //商品ID(前端传入)
try {
//加锁
boolean lock = releaseLock.lock(lockKey, "userId", 10);
if (!lock) {
return "系统繁忙,请稍后再试!";
}
//执行业务代码...
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int resultStock = stock - 1;
redisTemplate.opsForValue().set("stock", resultStock + "");
System.out.println("扣减成功,剩余库存:" + resultStock + "---当前IP和端口号地址:" + serverConfig.getUrl());
} else {
System.out.println("扣减失败,库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
return "error";
} finally {
//释放锁
releaseLock.releaseLock(lockKey, "userId");
}
return "success";
}
/**
* redisson分布式锁
* 最终方案:
* 使用redisson来实现分布式锁可以解决设置超时时间不合理问题
* 内置看门狗机制(不设置默认30秒,设置之后就不在触发看门狗机制)
* 监听策略:设置的超时时间/3,每10秒钟去看一下该线程是否还想持有这把锁,如果还想持有这把锁那么会再次把超时时间再次设置为30秒
*
* @return
*/
@RequestMapping("/deductStock6")
public String deductStock6() {
String lockKey = "product_001"; //商品ID(前端传入)
RLock redissonLock = redisson.getLock(lockKey);
try {
//加锁(启动看门狗机制)
redissonLock.lock();
//redissonLock.lock(30, TimeUnit.SECONDS);
//执行业务代码...
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int resultStock = stock - 1;
redisTemplate.opsForValue().set("stock", resultStock + "");
System.out.println("扣减成功,剩余库存:" + resultStock + "---当前IP和端口号地址:" + serverConfig.getUrl());
} else {
System.out.println("扣减失败,库存不足!");
}
} catch (Exception e) {
e.printStackTrace();
return "error";
} finally {
//释放锁
redissonLock.unlock();
}
return "success";
}
}