场景:若某商品库存量为50,存入redis,有一个功能:从redis将库存量读取出来,如果库存大于0,便对库存进行减一操作代表被买走一个,当库存小于0时提示库存不足
目录
搭建场景demo
查看Redis中的库存量
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件加入配置
redis 配置不配 database 默认值是0
spring:
redis:
enabled: true
database: 5
host: 127.0.0.1
port: 6379
password: 123456
timeout: 36000
connectionPoolSize: 1
connectionMinimumIdleSize: 1
接口功能如下
@GetMapping(value = "/test11")
public Boolean test11() {
String key = "stock";
//此处为自己写的工具类 直接使用stringRedisTemplate就可以
Integer stock = redisUtil.getCacheObject(key);
if (stock > 0){
stock = stock -1;
redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
System.out.println("库存成功减1, 剩余库存" + stock);
}else {
System.out.println("库存减少失败,库存不足");
}
return true;
}
发送请求测试该功能成功运行,查看redis库存,库存量减一
但是其实这样会出现一个问题:如果有两个或者多个请求同时请求该接口,A请求和B请求都拿到了自行车商品的库存量50减一存入库存数量就会是49,而实际情况是应该剩48辆,这样就导致了并发问题,可以进行并发测试去验证结果
解决办法
加锁,在一个请求过来的时候加锁去解决问题
Redis普通方法实现分布式锁
@GetMapping(value = "/test11")
public Boolean test11() {
String key = "stock";
String lockKey = "product101";
try{
//如果key存在就为false,且不会在存,不存在就会进行存入
Boolean product = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,
"product");
if (!product){
return false;
}
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
if (stock > 0){
stock = stock -1;
redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
System.out.println("库存成功减1, 剩余库存" + stock);
}else {
System.out.println("库存减少失败,库存不足");
}
}finally {
//最后要释放锁
stringRedisTemplate.delete(lockKey);
}
return true;
}
这样还是会导致问题
锁超时,如果有请求A进来,操作到一半突然挂掉,会导致该lockKey一直存在Redis中,这块资源就会一直被锁住,导致其他线程不能操作。解决办法:给key设置过期时间
设置过期时间之后还会导致问题,如果请求A进来之后操作时间大于设置的key的过期时间就会导致锁"不生效",A请求还没操作完毕,B请求就拿到了锁,到最后释放锁的时候其实A请求释放的是B请求的锁。解决办法:可以加一唯一标识设置到value,最后删除锁的时候去进行判断一下是否是A请求的锁,如果是才进行删除释放
@GetMapping(value = "/test11")
public Boolean test11() {
String key = "stock";
String lockKey = "product101";
UUID uuid = UUID.randomUUID();
//如果key存在就为false,且不会在存,不存在就会进行存入
Boolean product = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuid.toString());
try{
if (!product){
return false;
}
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
if (stock > 0){
stock = stock -1;
redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
System.out.println("库存成功减1, 剩余库存" + stock);
}else {
System.out.println("库存减少失败,库存不足");
}
}finally {
//最后要释放锁
if (Objects.equals(uuid,product)){
stringRedisTemplate.delete(lockKey);
}
}
return true;
}
虽然解决了以上的误删情况,但是还是没有解决同一时间有两个线程在操作该方法块,依然不完美。
解决办法: 利用Redission实现分布式锁
Redisson实现 分布式锁
学习参考链接 redisson 实现分布式锁
添加依赖
<!--Redis分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
配置RedissonClient
创建RedisProperties类,装配redis配置
@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private String host;
private int port;
private String password;
private int database;
}
创建RedissionConfig类,配置RedissonClient
@Configuration
@Log4j2
public class RedissionConfig {
@Resource
RedisProperties redisProperties;
@Bean
public RedissonClient redissonClient() {
RedissonClient redissonClient;
Config config = new Config();
System.out.println(redisProperties.getHost());
String url = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
config.useSingleServer().setAddress(url)
.setPassword(redisProperties.getPassword())
.setDatabase(redisProperties.getDatabase());
try {
redissonClient = Redisson.create(config);
return redissonClient;
}catch (Exception e){
log.error("RedissonClient init redis url" + url + " Exception " + e);
return null;
}
}
}
加锁解锁方法
从阻塞和非阻塞的特性来区分,加锁的方式又分为阻塞加锁和非阻塞加锁两类
阻塞加锁方法
-
void lock()
如果当前锁可用,则加锁成功,并立即返回如果当前锁不可用就一直阻塞等到锁可用,然后返回。 -
void lock(long leaseTime, TimeUnit unit)
加锁的机制是和void lock()
一致的,在此基础上增加了锁可用的时间,加锁成功后,如果没有调用显示unlock()
方法去解锁,到这个时间之后会自动解锁,如果leaseTime
传入-1则一直会持有直到unlock
非阻塞加锁方法
-
boolea tryLock()
调用该方法之后会立刻返回。返回值为true时则认为可用,加锁成功。返回值为false表示该锁不能使用,加锁失败 -
boolean tryLock(long time, TimeUnit unit)
如果锁可用立刻返回true,否则最多等待time
时间,如果time<=0
则不会等待,在time时间内锁可用则立刻返回true,time时间之后返回false。如果在等待期间线程被其他线程中断,则会抛出InterruptedException异常 -
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
与boolean tryLock(long time, TimeUnit unit)
相同,增加了过期时间
释放锁
void unlock()
释放锁。如果当前线程是锁的持有者(即在该锁实例上加锁成功的线程),则会释放成功,否则会抛出异常。
写一个解锁加锁的工具类
@Component
@Log4j2
public class RedissonLockUtil {
@Autowired
private RedissonClient redissonClient;
/**
* 加锁 (可重入锁) 阻塞加锁
*
* @param lockName 锁名
* @param expireTime 过期时间
* @param unit 时间粒度
* @return
*/
public Boolean lock(String lockName, long expireTime, TimeUnit unit){
try{
if (redissonClient == null){
log.error("redissonClient为空");
return false;
}
//获取锁实例
RLock lock = redissonClient.getLock(lockName);
//锁多少时间后自动释放,防止死锁
lock.lock(expireTime, unit);
log.info("线程" + Thread.currentThread().getName() + "加锁" + lockName + "成功");
return true;
}catch (Exception e){
log.error("加锁异常" + lockName + "Exception" + e);
return false;
}
}
/**
* 加锁 (可重入锁) 非阻塞加锁
*
* @param lockName 锁名
* @param expireTime 过期时间
* @param unit 时间粒度
* @param waitTime 等待时长
* @return
*/
public Boolean tryLock(String lockName, long expireTime, TimeUnit unit, long waitTime){
try {
if (redissonClient == null){
log.error("redissonClient为空");
return false;
}
RLock lock = redissonClient.getLock(lockName);
boolean b = lock.tryLock(waitTime, expireTime, unit);
if (b){
log.info("线程" + Thread.currentThread().getName() + "加锁" + lockName + "成功");
return true;
}
return false;
}catch (InterruptedException e){
log.error("加锁异常" + lockName + "Exception" + e);
return false;
}
}
/**
* 解锁
*
* @param lockName 锁名
* @return
*/
public Boolean unlock(String lockName) {
try{
if (redissonClient == null){
log.error("redissonClient为空");
return false;
}
RLock lock = redissonClient.getLock(lockName);
if (lock.isLocked()){
//判断当前线程释放的锁是否属于该线程
if (lock.isHeldByCurrentThread()) {
//主动释放锁
lock.unlock();
log.info("线程" + Thread.currentThread().getName() + "解锁" + lockName + "成功");
return true;
}
}
return true;
}catch (Exception e){
log.error(Thread.currentThread().getName() + "解锁异常:" + lockName);
return false;
}
}
}
测试(在高并发的情况下测试效果更加明显)
阻塞锁
@Resource
RedissonLockUtil redissonLockUtil;
@GetMapping(value = "/test15")
public Boolean test15() {
String key = "stock";
String lockKey = "lock";
Boolean lock = redissonLockUtil.lock(lockKey,30,TimeUnit.SECONDS);
if (lock){
Integer stock = redisUtil.getCacheObject(key);
if (stock == null){
return false;
}
if (stock > 0){
stock = stock -1;
redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
System.out.println("库存成功减1, 剩余库存" + stock);
redissonLockUtil.unlock(lockKey);
}else {
System.out.println("库存减少失败,库存不足");
}
}
return true;
}
运行结果
非阻塞锁
@Resource
RedissonLockUtil redissonLockUtil;
@GetMapping(value = "/test17")
public Boolean test17() {
String key = "stock";
String lockKey = "lock";
Boolean lock = redissonLockUtil.tryLock(lockKey,30,TimeUnit.SECONDS,50);
if (lock){
Integer stock = redisUtil.getCacheObject(key);
if (stock == null){
return false;
}
if (stock > 0){
stock = stock -1;
redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
System.out.println("库存成功减1, 剩余库存" + stock);
redissonLockUtil.unlock(lockKey);
}else {
System.out.println("库存减少失败,库存不足");
}
}
return true;
}
运行结果