SpringBoot集成Redis 一 分布式锁 与 缓存
欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot
1.添加依赖及配置(application.yml)
<!-- 引入redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
2.Redis分布式锁
Redis加锁和解锁的工具类
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 加锁
*
* @param key productId - 商品的唯一标志
* @param value 当前时间+超时时间 也就是时间戳
* @return
*/
public boolean lock(String key, String value) {
//锁不存在,未被占用,可以成功设置锁
if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
//锁存在,判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentValue = stringRedisTemplate.opsForValue().get(key);
//如果锁过期
if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
//获取旧锁,设置新锁
String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
//假如多个线程同时进入上面的if,但只有第一个线程获取的oldValue是上一个锁的currentValue,得以通过下一个if,获取锁
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
/**
* 解锁
*
* @param key
* @param value
*/
public void unlock(String key, String value) {
try {
String currentValue = stringRedisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
stringRedisTemplate.opsForValue().getOperations().delete(key);//删除key
}
} catch (Exception e) {
log.error("[Redis分布式锁] 解锁出现异常了,{}", e);
}
}
}
以上代码问题:
1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
2. 当锁过期的时候,如果多个客户端同时执行 jedis.getSet() 方法,
那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
3. 锁不具备拥有者标识,即任何客户端都可以解锁。
正确代码:
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作。
SET_WITH_EXPIRE_TIME:给这个key加一个过期时间的设置,具体时间由第五个参数决定
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean unLock(Jedis jedis, String lockKey, String requestId) {
//获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
参考:https://segmentfault.com/a/1190000015058486
分布式锁示例
@Service
public class SeckillServiceImpl implements SeckillService{
@Autowired
private RedisLock redisLock;
private static final int TIMEOUT = 10*1000;//超时时间 10s
/**
* 活动,特价,限量100000份
*/
static Map<String,Integer> products;//模拟商品信息表
static Map<String,Integer> stock;//模拟库存表
static Map<String,String> orders;//模拟下单成功用户表
static {
/**
* 模拟多个表,商品信息表,库存表,秒杀成功订单表
*/
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123456",100000);
stock.put("123456",100000);
}
private String queryMap(String productId){//模拟查询数据库
return "国庆活动,皮蛋特教,限量"
+products.get(productId)
+"份,还剩:"+stock.get(productId)
+"份,该商品成功下单用户数:"
+orders.size()+"人";
}
@Override
public String querySecKillProductInfo(String productId) {
return this.queryMap(productId);
}
/**
* synchronized锁方法是可以解决的,但是请求会变慢,
* 主要是没做到细粒度控制。
* 比如有很多商品的秒杀,但是这个把所有商品的秒杀都锁住了。而且这个只适合单机的情况,不适合集群
* @param productId
*/
@Override
public void orderProductMocckDiffUser(String productId) {
//加锁(不同的productId有不同的锁,达到细粒度的要求)
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
//未获取锁,说明有人正在进行操作
throw new SellException(101,"很抱歉,人太多了,换个姿势再试试~~");
}
//获取锁后
//1.查询该商品库存,为0则活动结束
int stockNum = stock.get(productId);
if(stockNum==0){
throw new SellException(100,"活动结束");
}else {
//2.下单
orders.put(KeyUtil.getUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try{
//不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。
// 虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
Thread.sleep(100);//模拟减库存的处理时间
}catch (InterruptedException e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
}
Spring Cache + Redis 简介
Spring Cache是一种缓存实现的通用技术,基于Spring提供的Cache框架。
Spring Cache提供了本身的简单实现NoOpCacheManager、ConcurrentMapCacheManager等。
开发者也可以通过Spring Cache,快速嵌入自己的Cache实现,如Redis等。
SpringCache是代码级的缓存,一般是使用一个ConcurrentMap。
也就是说实际上还是是使用JVM的内存来缓存对象的,那么肯定会造成大量的内存消耗,但是使用方便。
Redis 作为一个缓存服务器,是内存级的缓存。它是使用单纯的内存来进行缓存。
spring-boot-starter-data-redis 集成了 Spring Cache
Spring Cache + Redis 好处:
既可以很方便的缓存对象,同时用来缓存的内存的是使用redis的内存,不会消耗JVM的内存,提升了性能。
当然这里Redis不是必须的,换成其他的缓存服务器一样可以,只要实现Spring的Cache类,并配置到XML里面就行了。
Spring Cache + Redis 缓存
1. 方法返回的对象加了缓存注解的,一定要实现序列化!
2. springboot启动类上加上注解:@EnableCaching
@Cacheable
第一次访问会查询内容,方法会返回一个对象,返回对象的时候,会把这个对象存储。
下一次访问的时候,不会进去这个方法,直接从redis缓存中拿
value (也可使用 cacheNames):
可看做命名空间,表示存到哪个缓存里了。
key:
表示命名空间下缓存唯一key,使用Spring Expression Language生成。
condition:
表示在哪种情况下才缓存结果(对应的还有unless,哪种情况不缓存),同样使用SpEL
@Cacheable(value = "models", key = "#model.name", condition = "#model.name != ''")
public Model getForm(Model model) {
...
return model;
}
注:key如果不填,默认是空,对应的值应该就是方法的参数的值了。由于参数可能不同,使key不同,而无法达到更新的目的。所有key需要设置。
@CacheEvict
value (也可使用 cacheNames):
同Cacheable注解,可看做命名空间。表示删除哪个命名空间中的缓存
allEntries:
标记是否删除命名空间下所有缓存,默认为false
key:
同Cacheable注解,代表需要删除的命名空间下唯一的缓存key。
//访问这个方法之后删除对应的缓存,对应之前的Redis缓存注解的配置 。
@CacheEvict(cacheNames = "product",key = "123",allEntries = true)
public ModelAndView save(@Valid ProductForm productForm,BindingResult bindingResult,Map<String,Object> map){
...
}
@CachePut
@CachePut(cacheNames = "product",key = "123")
不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
一般使用在保存,更新方法中。
如果返回的是ModelAndView,由于ModelAndView无法序列化,返回ModelAndView不适合此注解。
可以到Service层注解或者Dao层注解@CachePut
Service使用@CachePut
在整个类上注解:
@CacheConfig(cacheNames = "product") //配置整个类的缓存cacheNames
Service方法注解:
@CachePut(key = "123")
public ProductInfo save(ProductInfo productInfo) {
return productInfoDao.save(productInfo);
}