一、SpringBoot集成Redisson
1.1查看Maven依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
</dependency>
1.2 SpringBoot容器注入Redisson
package com.yaodao.decorationdesign.config;
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: YaoDao
* @Package:
* @Description: 使用Redisson来实现分布式锁
* @Date: 2021/2/1 15:26
*/
@Configuration
public class RedissonConfig {
//使用properties中的配置
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String pwd;
@Bean
public Redisson redisson(){
//此为单机模式-SingleServer
Config config = new Config();
config.useSingleServer().setAddress("redis://"+host+":"+port).setDatabase(0).setPassword(pwd);
return (Redisson)Redisson.create(config);
}
}
二、业务中使用Redisson工具
主要就三行代码:
//1.获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
//2.加锁
redissonLock.lock();
//3.释放锁
redissonLock.unlock();
具体代码:
@Autowired
private Redisson redisson;
@PostMapping("/reduceStockByRedisson")
@ResponseBody
public Result reduceStockByRedisson(Integer id) throws Exception{
String lockKey = "lockKey";
//1.获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
try{
//2.使用redissonLock加锁
redissonLock.lock();
//加锁成功(获得了锁),那么执行业务逻辑
log.info("使用Redisson加锁成功!执行业务代码=>");
lockTestService.reduceStock(id);
return Result.ok().Message("购买成功!");
}catch (Exception e){
throw e;
}
finally {
//3.释放锁
redissonLock.unlock();
log.info("<=业务代码执行完毕,释放当前锁!");
}
}
三、实验,使用JMeter实现高并发访问场景
模拟场景,有20个用户同时向后台服务发送下订单的请求,并将20个请求通过nginx转发到两个后台服务中——实现分布式,现库存数量为15,正常情况下,当用户下一个订单,库存量减一,订单数加一,直到库存量为0;当请求全部处理且结束后,数据库中库存量为0,且订单数为15时,说明实验成功,反之实验失败。
3.1实验准备阶段
硬件与软件
window、云服务器
IDEA+Java、nginx、JMeter(windows下跑)
mysql、redis(云服务器),当然可以设置在本机win下
nginx配置80端口向8085、8086端口转发、开启nginx
nginx配置
#配置一个app名字
upstream app{
server localhost:8085;
server localhost:8086;
}
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://app/;
}
Idea运行两个服务类,分别在8085、8086端口
mysql数据库库存表:
mysql数据库订单表:
业务(下订单)代码:
/**
* @Author: YaoDao
* @Package:
* @Description:
* @Date: 2021/1/28 15:42
*/
@Service
@Slf4j
public class LockTestServiceImp implements LockTestService {
@Autowired
private LockTestMapper mapper;
/**
* create by: YaoDao
* description: 模拟购买 减库存,加订单
* create time: 2021/1/28 15:45
* @Param: 商品id
* @return
*/
@Transactional
public void reduceStock(Integer id){
//1.获取库存
LockTestProduct product = mapper.getProduct(id);
//模拟耗时业务
sleep(1000);
if(product.getNumber()<=0){
log.info("所购商品已无库存!");
throw new RuntimeException("所购商品已无库存!");
}
//2.如果库存还有就减库存
if(mapper.updateProduct(id)>0){
LockTestOrder order = new LockTestOrder();
order.setUserId("YaoDao");
order.setPid(id);
mapper.insertOrder(order);
log.info("成功下单!,产品信息:{},订单信息:{}",product,order);
}else {
log.info("减库存操作失败!");
throw new RuntimeException("减库存操作失败!");
}
}
/**
* create by: YaoDao
* description: 模拟耗时业务
* create time: 2021/1/28 15:55
* @Param: null
* @return
*/
private void sleep(int i) {
long time = Long.parseLong(String.valueOf(i));
try{
Thread.sleep(time);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
mapper代码:
接口
package com.yaodao.decorationdesign.dao.Test;
import com.yaodao.decorationdesign.object.entity.Test.LockTest.LockTestOrder;
import com.yaodao.decorationdesign.object.entity.Test.LockTest.LockTestProduct;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* @Author: YaoDao
* @Package:
* @Description: 该接口用于操作 数据库中的lock_test_product和lock_test_order表
* 用于实验高并发下 如何对数据库中的信息处理 以库存表stock_table为例
* 模拟场景 高并发下对商品的抢购
* @Date: 2021/1/28 15:05
*/
@Repository
@Mapper
public interface LockTestMapper {
/**
* create by: YaoDao
* description: 获取产品的信息(库存信息)
* create time: 2021/1/28 15:10
* @Param: null
* @return
*/
LockTestProduct getProduct(@Param("id") Integer id);
/**
* create by: HaoNan
* description: 在原基础的库存上减1
* create time: 2021/1/28 15:37
* @Param: null
* @return
*/
int updateProduct(@Param("id") Integer id);
/**
* create by: YaoDao
* description: 生成订单
* create time: 2021/1/28 15:36
* @Param: null
* @return
*/
@Options(useGeneratedKeys = true,keyColumn = "id",keyProperty = "id")
@Insert("insert into lock_test_order(pid,user_id) values(#{pid},#{userId})")
int insertOrder(LockTestOrder order);
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yaodao.decorationdesign.dao.Test.LockTestMapper">
<update id="updateProduct">
update lock_test_product set number = number-1 where id = #{id}
</update>
<select id="getProduct" resultType="com.yaodao.decorationdesign.object.entity.Test.LockTest.LockTestProduct">
select * from lock_test_product where id = #{id}
</select>
</mapper>
3.2带有缺陷的Redis锁实验
实验代码
/**
* @Author: YaoDao
* @Package: 模拟高并发减库存 的入口
* @Description:
* @Date: 2021/1/28 15:56
*/
@RestController
@RequestMapping("/test/lockTest")
@Slf4j
public class LockTestController {
@Autowired
private LockTestService lockTestService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@PostMapping("/reduceStockByRedisLock")
@ResponseBody
public Result reduceStockByRedisLock(Integer id) throws Exception{
String lockKey = "lockKey";
String threadId = UUID.randomUUID().toString();
try{
//使用setIfAbsent方法,如果没有这个key值则set加上一个Key,返回True,当作加锁成功。如果有这个Key,那就返回False;
//Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"redisLock");
//如果获得锁之后,执行业务代码时宕机了,那么这个锁就会一直存在。为了避免死锁,为这把锁加一个过期时间。
//stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
//将上面两行代码变成一条原子指令
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,threadId,10, TimeUnit.SECONDS);
if(!isLocked){
//加锁未成功
throw new RuntimeException("系统繁忙请稍后再试!");
}
//加锁成功(获得了锁),那么执行业务逻辑
log.info("加锁成功!执行业务代码=>");
lockTestService.reduceStock(id);
return Result.ok().Message("购买成功!");
}catch (Exception e){
throw e;
}
finally {
if (threadId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
//只删掉自己设置的锁
stringRedisTemplate.delete(lockKey);
log.info("<=业务代码执行完毕,释放当前线程锁!"+threadId);
}
}
}
}
JMeter请求
设置10个线程
JMeter请求结果和控制台日志
虽然没有出现超卖问题,但是同时访问时,由于没有设置循环请求获得锁,导致只有一个线程获得锁,成功执行了业务。当然可以添加while循环去循环不断请求拿到锁,这里就不进行实验了。
3.3使用Redisson锁
代码
package com.yaodao.decorationdesign.controller.Test;
import com.yaodao.decorationdesign.service.Test.LockTestService;
import com.yaodao.decorationdesign.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @Author: YaoDao
* @Package: 模拟高并发减库存 的入口
* @Description:
* @Date: 2021/1/28 15:56
*/
@RestController
@RequestMapping("/test/lockTest")
@Slf4j
public class LockTestController {
@Autowired
private LockTestService lockTestService;
@Autowired
private Redisson redisson;
@PostMapping("/reduceStockByRedisson")
@ResponseBody
public Result reduceStockByRedisson(Integer id) throws Exception{
String lockKey = "lockKey";
//1.获取锁对象
RLock redissonLock = redisson.getLock(lockKey);
try{
//2.使用redissonLock加锁
redissonLock.lock();
//加锁成功(获得了锁),那么执行业务逻辑
log.info("使用Redisson加锁成功!执行业务代码=>");
lockTestService.reduceStock(id);
return Result.ok().Message("购买成功!");
}catch (Exception e){
throw e;
}
finally {
//3.释放锁
redissonLock.unlock();
log.info("<=业务代码执行完毕,释放当前锁!");
}
}
}
JMeter(线程设置为20):
实验结果:
日志结果:
数据库:
满足实验性结果。证明Redisson是可以在高并发场景作为分布式锁的。
四、原理分析
4.1Redisson分布式锁实现原理流程图
用户进行下订单操作,调用后台服务,执行业务代码前进行加锁操作,如果加锁成功则执行业务代码,并同时开启一个后台线程来不断给锁续命,以免导致业务代码执行结束前锁就失效了;反之,如果加锁失败,则一直尝试加锁,不断循环。
4.2源码分析
不好意思,还没看懂。
最后
欢迎大家到我的个人博客下找我哦:www.yaodao666.xyz
同时希望大家点个赞,支持一下原创博文,多谢!