文章目录
Redis 实用
springboot整合redis
springboot版本
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
导入redis包
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties配置
# redis配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.236.140
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间
spring.redis.timeout=1000ms
# 缓存超时时间
spring.cache.redis.time-to-live=-1ms
配置RedisTemplate(自定义序列化和使用缓存)
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @author yeming.gao
* @Description: redis配置类
* 继承CachingConfigurerSupport,为了自定义生成KEY的策略。可以不继承。
* 注解@EnableCaching 启用缓存,这个注解很重要;
* @date 2020/8/27 15:05
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.cache.redis.time-to-live}")
private Duration timeToLive = Duration.ZERO;
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(stringRedisSerializer);
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(timeToLive)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
测试
import com.yeming.tinyid.application.TinyidApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @author yeming.gao
* @Description: redis测试类
* @date 2020/8/27 15:13
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TinyidApplication.class)
public class RedisTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
public void hashTest() {
String key = "mapKey";
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
redisTemplate.delete(key);
}
Map<String, String> map = new HashMap<>();
map.put("name", "高业明");
map.put("age", "25");
//redis中添加hash
redisTemplate.opsForHash().putAll(key, map);
//获取hash
Map<Object, Object> hash = redisTemplate.opsForHash().entries(key);
System.out.println(hash.toString());
}
}
StringRedisTemplate与RedisTemplate
spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在RedisTemplate中提供了几个常用的接口方法的使用,分别是:
private ValueOperations<K, V> valueOps;
private HashOperations<K, V> hashOps;
private ListOperations<K, V> listOps;
private SetOperations<K, V> setOps;
private ZSetOperations<K, V> zSetOps;
RedisTemplate中定义了对5种数据结构操作
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
Redis客户端命令对应的RedisTemplate中的方法列表
String类型结构 | |
---|---|
Redis | RedisTemplate rt |
set key value | rt.opsForValue().set(“key”,“value”) |
get key | rt.opsForValue().get(“key”) |
del key | rt.delete(“key”) |
strlen key | rt.opsForValue().size(“key”) |
getset key value | rt.opsForValue().getAndSet(“key”,“value”) |
getrange key start end | rt.opsForValue().get(“key”,start,end) |
append key value | rt.opsForValue().append(“key”,“value”) |
Hash结构 | |
hmset key field1 value1 field2 value2… | rt.opsForHash().putAll(“key”,map) //map是一个集合对象 |
hset key field value | rt.opsForHash().put(“key”,“field”,“value”) |
hexists key field | rt.opsForHash().hasKey(“key”,“field”) |
hgetall key | rt.opsForHash().entries(“key”) //返回Map对象 |
hvals key | rt.opsForHash().values(“key”) //返回List对象 |
hkeys key | rt.opsForHash().keys(“key”) //返回List对象 |
hmget key field1 field2… | rt.opsForHash().multiGet(“key”,keyList) |
hsetnx key field value | rt.opsForHash().putIfAbsent(“key”,“field”,“value” |
hdel key field1 field2 | rt.opsForHash().delete(“key”,“field1”,“field2”) |
hget key field | rt.opsForHash().get(“key”,“field”) |
List结构 | |
lpush list node1 node2 node3… | rt.opsForList().leftPush(“list”,“node”) |
rt.opsForList().leftPushAll(“list”,list) //list是集合对象 | |
rpush list node1 node2 node3… | rt.opsForList().rightPush(“list”,“node”) |
rt.opsForList().rightPushAll(“list”,list) //list是集合对象 | |
lindex key index | rt.opsForList().index(“list”, index) |
llen key | rt.opsForList().size(“key”) |
lpop key | rt.opsForList().leftPop(“key”) |
rpop key | rt.opsForList().rightPop(“key”) |
lpushx list node | rt.opsForList().leftPushIfPresent(“list”,“node”) |
rpushx list node | rt.opsForList().rightPushIfPresent(“list”,“node”) |
lrange list start end | rt.opsForList().range(“list”,start,end) |
lrem list count value | rt.opsForList().remove(“list”,count,“value”) |
lset key index value | rt.opsForList().set(“list”,index,“value”) |
Set结构 | |
sadd key member1 member2… | rt.boundSetOps(“key”).add(“member1”,“member2”,…) |
rt.opsForSet().add(“key”, set) //set是一个集合对象 | |
scard key | rt.opsForSet().size(“key”) |
sidff key1 key2 | rt.opsForSet().difference(“key1”,“key2”) //返回一个集合对象 |
sinter key1 key2 | rt.opsForSet().intersect(“key1”,“key2”)//同上 |
sunion key1 key2 | rt.opsForSet().union(“key1”,“key2”)//同上 |
sdiffstore des key1 key2 | rt.opsForSet().differenceAndStore(“key1”,“key2”,“des”) |
sinter des key1 key2 | rt.opsForSet().intersectAndStore(“key1”,“key2”,“des”) |
sunionstore des key1 key2 | rt.opsForSet().unionAndStore(“key1”,“key2”,“des”) |
sismember key member | rt.opsForSet().isMember(“key”,“member”) |
smembers key | rt.opsForSet().members(“key”) |
spop key | rt.opsForSet().pop(“key”) |
srandmember key count | rt.opsForSet().randomMember(“key”,count) |
srem key member1 member2… | rt.opsForSet().remove(“key”,“member1”,“member2”,…) |
redis实现分布式锁
原理
对于每个线程持有的锁进行标记,表示该锁只能被该线程释放掉;其他线程不允许操作。这里需要考虑分布式环境,所以一个锁关键字再这里不适用。这里锁获取不到线程会继续等待,不想等待可以自行更改。
锁接口
/**
* @author yeming.gao
* @Description: 分布式锁功能接口
* @date 2020/8/28 10:00
*/
public interface Lock {
/**
* 获取锁
* @param lock 锁的名称
*/
void lock(String lock);
/**
* 释放锁
* @param lock 锁的名称
*/
void unlock(String lock);
}
java实现(存在缺陷,代码已经介绍)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author yeming.gao
* @Description: Redis 分布式锁的实现
* @date 2020/8/28 10:02
*/
@Component
public class RedisDistributeLock implements Lock {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisDistributeLock.class);
/**
* 锁的前缀
**/
private static final String LOCK_PREFIX = "distribute_lock_";
/**
* 锁的过期时间,单位秒
**/
private static final int LOCK_MAX_EXIST_TIME = 5;
/**
* 线程ID前缀,可以使用当前应用名用于取分
**/
private static final String THREAD_PREFIX = "yeming_";
/**
* 标记每个线程ID
**/
private static final ThreadLocal<String> THREAD_ID = new ThreadLocal<>();
/**
* 用于生成随机数
**/
private static final Random RANDOM = new Random();
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 实现可重入锁
* 活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,
* 线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。
* 这样你让我,我让你,最后两个线程都无法使用资源。
*
* @param lock 锁的名称
*/
@Override
public void lock(String lock) {
Assert.notNull(lock, "锁名称不能为空");
//获取锁的key
final String lockKey = generatorLockKey(lock);
//绑定分布式锁key的redis类型对象
BoundValueOperations<String, Object> keyBoundValueOperations = redisTemplate.boundValueOps(lockKey);
while (true) {
// 获取分布式锁的值
Object lockValue = keyBoundValueOperations.get();
// 根据传入的值,判断用户是否持有该锁
if (Objects.nonNull(lockValue) && lockValue.equals(THREAD_ID.get())) {
// 重置该锁的过期时间
keyBoundValueOperations.expire(LOCK_MAX_EXIST_TIME, TimeUnit.SECONDS);
break;
}
// 尝试获取锁;redis的setNX
if (Boolean.TRUE.equals(keyBoundValueOperations.setIfAbsent(lockKey))) {
// 生成当前线程ID
final String threadUUID = THREAD_PREFIX + UUID.randomUUID().toString();
THREAD_ID.set(threadUUID);
//设置锁的值
keyBoundValueOperations.set(threadUUID);
// 重置该锁的过期时间;防止一个线程拿到锁之后,该锁没有得到正常释放;不设置过期时间,该锁会一直锁下去影响正常业务
keyBoundValueOperations.expire(LOCK_MAX_EXIST_TIME, TimeUnit.SECONDS);
LOGGER.info("lockKey=[{}]成功获取锁,threadID=[{}]", lockKey, threadUUID);
break;
} else {
// 没有获取锁则尝试等待
try {
//短暂休眠,nano避免活锁;当很多线程进来同一时间去获取锁,这个时候就可能出现活锁
Thread.sleep(RANDOM.nextInt(50));
} catch (InterruptedException e) {
LOGGER.warn("线程中断发生异常:", e);
//手动中断线程
Thread.currentThread().interrupt();
//break;不能break,不然可能视为已经获取锁了;锁没有意义,所以需要继续尝试获取锁
}
}
}
}
/**
* 释放可重入锁
* <p>
* 该方法存在缺陷:
* 在本线程获取值,并判断成功该锁为本线程所有,但是在执行删除操作之前,锁超时被释放的同时被另一个线程所有,那么本线程就会错误的释放锁
* <p>
* 解决办法:
* 使用lua脚本,保证了检测和删除在同一事务中,保证了一个方法的原子性
*
* @param lock 锁的名称
*/
@Override
public void unlock(String lock) {
Assert.notNull(lock, "锁名称不能为空");
//获取锁的key
final String lockKey = generatorLockKey(lock);
//绑定分布式锁key的redis类型对象
BoundValueOperations<String, Object> keyBoundValueOperations = redisTemplate.boundValueOps(lockKey);
// 获取分布式锁的值
Object lockValue = keyBoundValueOperations.get();
// 根据传入的值,判断用户是否持有该锁
if (Objects.nonNull(lockValue) && lockValue.equals(THREAD_ID.get())) {
// 此处不是绝对的安全
// (当这里判断通过时,同时锁由于超时被删除,这时候另一个线程获取了锁,这个时候再执行删除操作就会导致该线程删除了错误的锁;这里不是原子操作)
// 释放锁
redisTemplate.delete(lockKey);
}
LOGGER.info("lockKey=[{}]持有的锁释放成功,threadID=[{}]", lockKey, lockValue);
THREAD_ID.remove();
}
/**
* 生成带前缀的锁的key
*
* @param lock 锁的名称
* @return String
*/
private String generatorLockKey(String lock) {
return LOCK_PREFIX + lock;
}
}
lua脚本实现(lua脚本都是原子操作,保证了绝对的锁持有的安全性)
lock.lua
---
--- 分布式获取锁
--- Created by yeming.gao.
--- DateTime: 2020/8/28 13:45
---
local key = KEYS[1] -- 锁的key
local threadId = KEYS[2] -- 锁的值(线程ID)
local expireTime = ARGV[1] -- 锁的过期时间
local lockSet = redis.call('setnx', key, threadId) -- 调用redis的setnx获取锁
if lockSet == 1 then
-- 获取锁成功
redis.call('expire', key, expireTime) -- 更新锁的过期时间
else
-- 获取锁失败
local lockValue = redis.call('get', key) -- 获取锁的值
if lockValue == threadId then
-- 如果锁的值等于传入threadId
lockSet = 1
redis.call('expire', key, expireTime) -- 更新锁的过期时间
end
end
return lockSet`
unlock.lua
---
--- 分布式释放锁
--- Created by yeming.gao.
--- DateTime: 2020/8/28 13:45
---
local key = KEYS[1] -- 锁的key
local threadId = KEYS[2] -- 锁的值(线程ID)
local lockValue = redis.call('get', key) -- 获取锁的值
if lockValue == threadId then
-- 如果锁的值等于传入threadId
return redis.call('del', key) -- 删除锁
end
return 0
java执行lua脚本
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
/**
* @author yeming.gao
* @Description: lua脚本实现分布式锁
* @date 2020/8/28 13:58
*/
@Component
public class RedisDistributeLockLua implements Lock {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisDistributeLockLua.class);
/**
* 锁的前缀
**/
private static final String LOCK_PREFIX = "distribute_lock_";
/**
* 锁的过期时间,单位秒
**/
private static final int LOCK_MAX_EXIST_TIME = 5;
/**
* 线程ID前缀,可以使用当前应用名用于取分
**/
private static final String THREAD_PREFIX = "yeming_";
/**
* 标记每个线程ID
**/
private static final ThreadLocal<String> THREAD_ID = new ThreadLocal<>();
/**
* 用于生成随机数
**/
private static final Random RANDOM = new Random();
/**
* lua加锁脚本
*/
private DefaultRedisScript<Long> lockRedisScript;
/**
* lua解锁脚本
*/
private DefaultRedisScript<Long> unlockRedisScript;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@PostConstruct
public void init() {
LOGGER.info("lua脚本初始化");
//lock脚本初始化
lockRedisScript = new DefaultRedisScript<>();
lockRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
lockRedisScript.setResultType(Long.class);
//unlock脚本初始化
unlockRedisScript = new DefaultRedisScript<>();
unlockRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
unlockRedisScript.setResultType(Long.class);
}
@Override
public void lock(String lock) {
Assert.notNull(lock, "锁名称不能为空");
//获取锁的key
final String lockKey = generatorLockKey(lock);
while (true) {
List<String> keys = new ArrayList<>();
keys.add(lockKey);
// 生成当前线程ID
final String threadUUID = THREAD_PREFIX + UUID.randomUUID().toString();
THREAD_ID.set(threadUUID);
keys.add(THREAD_ID.get());
Long luaExec = redisTemplate.execute(lockRedisScript, keys, LOCK_MAX_EXIST_TIME * 100);
if (Long.valueOf(0).equals(luaExec)) {
LOGGER.info("lockKey=[{}]成功获取锁,threadID=[{}]", lockKey, THREAD_ID.get());
break;
} else {
// 没有获取锁则尝试等待
try {
//短暂休眠,nano避免活锁;当很多线程进来同一时间去获取锁,这个时候就可能出现活锁
Thread.sleep(RANDOM.nextInt(50));
} catch (InterruptedException e) {
LOGGER.warn("线程中断发生异常:", e);
//手动中断线程
Thread.currentThread().interrupt();
//break;不能break,不然可能视为已经获取锁了;锁没有意义,所以需要继续尝试获取锁
}
}
}
}
@Override
public void unlock(String lock) {
Assert.notNull(lock, "锁名称不能为空");
//获取锁的key
final String lockKey = generatorLockKey(lock);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
keys.add(THREAD_ID.get());
redisTemplate.execute(unlockRedisScript, keys);
THREAD_ID.remove();
}
/**
* 生成带前缀的锁的key
*
* @param lock 锁的名称
* @return String
*/
private String generatorLockKey(String lock) {
return LOCK_PREFIX + lock;
}
}
相关文章
Redis6.0.6_01_Redis安装教程
Redis6.0.6_02_Redis 入门基础
Redis6.0.6_03_Redis 实用
Redis6.0.6_04_Redis 主从复制与哨兵模式
Redis6.0.6_05_Redis管道和Lua脚本
Redis6.0.6_06_Redis 集群
Redis6.0.6_07_Redis 开发须知