1、pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.com.demo</groupId>
<artifactId>SpringBootDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- spring-boot-starter-parent-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
</parent>
<dependencies>
<!-- spring-boot-starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--访问静态资源-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--集成Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.6</version>
</dependency>
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-start-cache</artifactId>-->
<!-- </dependency>-->
<!--redis数据序列化-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- slf4j和logj-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
</dependency>
<!-- lombook-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
2、application.properties
# redis集群配置
spring.redis.cluster.nodes=127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
#获取数据超时
spring.redis.soTimeout=2000
#获取数据超时最大尝试次数
spring.redis.maxAttempts=5
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=20
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=20
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=2000
3、JedisClusterConfig
(1)配置JedisCluster(Redis集群) Bean对象
JedisCluster(节点集合,连接超时时间,获取数据超时时间,最大尝试次数,密码,连接池配置JedisPoolConfig)
(2)RedisConnectionFactory,根据JedisCluster的配置(通过spring-boot-starter-data-redis中的实现)能够自动生成RedisConnectionFactory Bean对象
(3)配置RedisTemplate,通过设置RedisConnectionFactory和设置key和value的序列化方法。
package cn.com.demo.redis;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashSet;
import java.util.Set;
@Configuration
public class JedisClusterConfig{
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.soTimeout}")
private int soTimeout;
@Value("${spring.redis.maxAttempts}")
private int maxAttempts;
@Value("${spring.redis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.pool.max-wait}")
private long maxWait;
//自动注入,根据JedisCluster中的信息生成
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public JedisCluster getJedisCluster() {
return new JedisCluster(getNodes(), timeout,soTimeout,maxAttempts,password,poolConfig());
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(){
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//key
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//设置value 自定义的序列化
template.setDefaultSerializer(fastJsonRedisSerializer());
//设置key序列化方式 String序列化
template.setKeySerializer(stringRedisSerializer);
return template;
}
// @Bean
// @Override
// public CacheManager cacheManager() {
// // 初始化一个RedisCacheWriter
// RedisCacheWriter cacheManager = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// // 设置默认过期时间:2 分钟
// RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// .entryTtl(Duration.ofMinutes(1))
// .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer()));
// RedisCacheManager redisCacheManager = new RedisCacheManager(cacheManager, defaultCacheConfig);
// return redisCacheManager;
// }
//
public FastJsonRedisSerializer<Object> fastJsonRedisSerializer() {
//允许反序列化
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.WriteEnumUsingToString,
SerializerFeature.WriteClassName);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
fastJsonRedisSerializer.setFastJsonConfig(fastJsonConfig);
return fastJsonRedisSerializer;
}
/**
* 连接池配置
* @return
*/
private JedisPoolConfig poolConfig() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setMaxTotal(maxActive);
config.setMaxWaitMillis(maxWait);
return config;
}
/**
* 获取集群节点数组
* @return
*/
private Set<HostAndPort> getNodes() {
String[] cNodes = clusterNodes.split(",");
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
// 分割出集群节点
String[] hp;
for (String node : cNodes) {
hp = node.split(":");
nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
}
return nodes;
}
}
4、工具类
通过RedisTemplate来执行redis指令
(1)通过RedisTemplate.opsFor****()获取对应具体数据类型(String,List,Hash,Set,Zset及高级数据结构)的操作(指令)集合;
redisTemplate.expire(key, expireTime, timeUnit); //设置KEY的超时时间
redisTemplate.keys(pattern);// KEYS [pattern]
redisTemplate.delete(keys);// DELETE [key]
redisTemplate.hasKey(key);//判断key是否存在
(2)通过****Operations<Serializable, Object>来执行一条具体的指令:
如:
//对应指令SETNX
result = operations.setIfAbsent(key, value);
package cn.com.demo.redis;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
@Service
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
/**
* 执行lua脚本
* @param redisScript lua脚本
* @param keys key Key链表与 KEYS[i]对应
* @param values value
* @return
*/
public Object executeLua(RedisScript redisScript,List<String> keys,Object... values){
Object result=redisTemplate.execute(redisScript,keys,values);
return result;
}
/**
* setNx
* 如果key不存在则插入,存在直接返回false
* @param key
* @param value
* @return
*/
public boolean setNx(final String key, Object value) {
boolean result = false;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.setIfAbsent(key, value);
return result;
}
/**
* getSet
* 用于设置给定key的值,并返回key的旧值
* @param key
* @param value
* @return
*/
public Object getSet(final String key, Object value) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.getAndSet(key, value);
return result;
}
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
return result;
}
/**
* 写入缓存设置时效时间
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value, Long expireTime, TimeUnit timeUnit) {
boolean result = false;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, timeUnit);
result = true;
return result;
}
/**
* 批量删除对应的value
*
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 批量删除key
*
* @param pattern
*/
public void removePattern(final String pattern) {
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size() > 0) {
redisTemplate.delete(keys);
}
}
/**
* 删除对应的value
*
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的value
*
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
*
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 哈希 添加
*
* @param key
* @param hashKey
* @param value
*/
public void hmSet(String key, Object hashKey, Object value) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key, hashKey, value);
}
/**
* 哈希获取数据
*
* @param key
* @param hashKey
* @return
*/
public Object hmGet(String key, Object hashKey) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key, hashKey);
}
/**
* 列表添加
*
* @param k
* @param v
*/
public void lPush(String k, Object v) {
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(k, v);
}
/**
* 列表获取
*
* @param k
* @param l
* @param l1
* @return
*/
public List<Object> lRange(String k, long l, long l1) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return list.range(k, l, l1);
}
/**
* 集合添加
*
* @param key
* @param value
*/
public void add(String key, Object value) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key, value);
}
/**
* 集合获取
*
* @param key
* @return
*/
public Set<Object> setMembers(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}
/**
* 有序集合添加
*
* @param key
* @param value
* @param scoure
*/
public void zAdd(String key, Object value, double scoure) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.add(key, value, scoure);
}
/**
* 有序集合获取
*
* @param key
* @param scoure
* @param scoure1
* @return
*/
public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.rangeByScore(key, scoure, scoure1);
}
}
5、通过RedisTemplate执行lua脚本
(1)redislua1.lua
if redis.call('EXISTS',KEYS[1])==1
then
redis.call('set',KEYS[1],ARGV[2])
else
redis.call('set',KEYS[1],ARGV[1])
end
return redis.call('get',KEYS[1])
(2)加载脚本执行
a、通过RedisTemplate.execute()在集群执行lua脚本必须保证所有的key在同一个槽(Redis实例),即lua脚本要在一个实例上运行,否则会报错CROSSSLOT Keys in request don't hash to the same slot
b、要通过 redisScript.setResultType(PersonInfo.class);设置返回值的类型,否则返回为空。
public void luaTest(){
//调用lua脚本并执行
DefaultRedisScript<PersonInfo> redisScript = new DefaultRedisScript<PersonInfo>();
//lua文件存放在resources目录下的redis文件夹内
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redislua1.lua")));
//设置返回值的类型,不设置会返回null
redisScript.setResultType(PersonInfo.class);
List<String> keys=new ArrayList<>();
keys.add("LuaPersonInfo");
PersonInfo defaultPerson=new PersonInfo("DefaultPerson",20,"Shanghai");
PersonInfo personInfo1=new PersonInfo("Tom",10,"Beijing");
Object result=redisUtils.executeLua(redisScript,keys,defaultPerson,personInfo1);
if(result!=null){
log.info(result.toString());
}
}
运行结果:
6、Redis实现分布式锁一(通过setNX和getSet实现)
package cn.com.demo.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalTime;
/**
* 分布式锁的实现
*/
@Service
public class DistributeLockUtil {
@Autowired
private RedisUtils redisUtils;
/**
* 获取分布式锁
*
* @param lockName 锁名称
* @param expireTime 超时时间(秒)
* @return
*/
public Boolean getDistributeLock(String lockName, long expireTime) {
while(true){
LocalTime localTime = LocalTime.now();
localTime=localTime.plusSeconds(expireTime);
//1、通过setNx来获取锁
boolean getSuccess = redisUtils.setNx(lockName, localTime);
if (getSuccess) {
//2.1、获取锁成功
return true;
}
//2.2获取锁失败
//3、判断是锁是否被正常持有
//3.1获取锁的当前拥有者,持有锁的有效时间
String vailTimeStr=(String)redisUtils.get(lockName);
//重新获取锁发现已经被释放,重新开始运行
if(vailTimeStr==null){
continue;
}
LocalTime vailTime =LocalTime.parse(vailTimeStr);
localTime = LocalTime.now();
//3.2 判断有效时间是否在当前时间之后,在之后表明锁被其他线程正常持有,在之前说明锁的当前持有者发生了异常
if (vailTime.isAfter(localTime)) {
//被其他线程正常持有,当前线程获取锁失败
return false;
}
//4、被其他线程异常持有,当前线程去重新获取,为了防止在当前线程获取的同时,其他线程先获取,使用getAndSet()来
String oldTimeStr= (String)redisUtils.getSet(lockName,localTime.plusSeconds(expireTime));
if(vailTimeStr.equals(oldTimeStr)){
//没有其他线程在当前线程之前获取到锁
return true;
}
//在当前线程获取锁之前,已经有其他线程获取到了锁
// System.out.println("在当前线程获取锁之前,已经有其他线程获取到了锁");
return false;
}
}
/**
* 释放锁
* @param lockName
*/
public void releaseDistributeLock(String lockName){
redisUtils.remove(lockName);
}
}
public void doProcess(String lockName,int time) throws InterruptedException {
while (true){
if(distributeLockUtil.getDistributeLock(lockName,5)){
log.info(Thread.currentThread().getName()+"获取锁成功");
Thread.sleep(time*1000);
log.info(Thread.currentThread().getName()+"休眠后,释放锁");
distributeLockUtil.releaseDistributeLock(lockName);
return;
}else{
log.info(Thread.currentThread().getName()+"获取锁失败,短暂休眠后重新请求锁");
Thread.sleep(1000);
}
}
}
(1) Key存放锁的名称,Value存放当前锁的过期时间点(自己计算过期时间)
(2)通过String的SETNX指令上锁,返回true上锁成功
(3)返回false表示上锁失败,上锁失败即其他线程正持有锁,此时其他线程可能存在两种情况:第一种持有锁的线程线程正常在执行任务;第二种持有锁的线程(或实例)异常但是没来的及释放锁。通过锁的过期时间点在当前时间之前还是之后来判断(在当前时间之后,认为正在正常运行;在当前时间之前,认为持有锁的线程异常不能及时释放锁)
(4)持有锁的线程正常,返回false表示此次获取锁失败
(5)持有锁的线程异常,则当前线程去强抢锁。去强抢锁时要注意可能有其他的线程此时也来强抢锁,通过cas(Redis的getSet指令)来保证线程强抢锁的线程安全:首先通过get()指令获取到过期时间t1,然后通过getSet将设置t3,同时返回此时的值t2,如果t1=t2,就说明这个过程中没有其他线程来强抢锁,当前线程获取锁成功;如果t1!=t2则说明在当前线程强抢锁之前已经有线程强抢到了锁,当前线程获取锁失败。
(6)删除锁直接通过redis的delete指令,锁只能是持有的线程手动的释放,或不释放请求的线程 强抢
7、Redis实现分布式锁二(SETNX和lua脚本)
(1)value为持有锁的线程标识,通过redis实现锁的超时过期
(2)锁是被持有线程手动释放的或redis的过期删除策略自动释放
(3)通过setnx上锁,设置过期时间及所持有线程,成功返回true,失败返回false
(4)通过lua脚本释放锁,使用lua脚本是使判断指令和删除指令之间的操作具有原子性
(5)lua脚本:
redisLock.lua
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
package cn.com.demo.redis;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 通过SETNX和lua脚本实现
*/
@Service
public class DistributeLockUtil2 {
public final static String serverInstanceName="DemoServer1_";
@Autowired
private RedisUtils redisUtils;
/**
* 上锁
* @param lockName
* @return
*/
public Boolean getLock(String lockName,Long time,TimeUnit timeUnit){
//1.通过setNX获取锁,Value为持有锁的线程名称
Boolean result=redisUtils.setNxEx(lockName,serverInstanceName+Thread.currentThread().getName(),time, timeUnit);
if(result){
//setnx成功表示获取锁成功
//TODO:添加定时任务去刷新锁的有效时间
}
return result;
}
/**
* 锁的状态
* (1)锁不存在了,即任务已经超时了,无需释放锁,返回false
* (2)锁仍然存在:
* (2.1)value还是当前的线程,正常运行,手动释放锁,返回true
* (2.2)value已经别为了其他线程,当前线程运行超时,锁被redis自动释放,不能释放锁,返回false
* @param lockName
* @return
*/
public Boolean releaseLock(String lockName){
//1.获取lua脚本
DefaultRedisScript<Boolean> redisScript=new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redisLock.lua")));
//2.执行lua脚本(原子),判断当前锁的
List<String> keys=new ArrayList<>();
keys.add(lockName);
Boolean result=(Boolean)redisUtils.executeLua(redisScript,keys,DistributeLockUtil2.serverInstanceName+Thread.currentThread().getName());
return result;
}
}
public void doProcess2(String lockName,int time) throws InterruptedException {
while (true){
if(distributeLockUtil2.getLock(lockName,10l, TimeUnit.SECONDS)){
log.info(Thread.currentThread().getName()+"获取锁成功");
Thread.sleep(time*1000);
log.info(Thread.currentThread().getName()+"休眠后,释放锁");
distributeLockUtil2.releaseLock(lockName);
return;
}else{
log.info(Thread.currentThread().getName()+"获取锁失败,短暂休眠后重新请求锁");
Thread.sleep(1000);
}
}
}
8、两种实现的区别
(1)释放锁的方式:一种是手动释放加不释放强占;另一种是手动释放加Redis自动释放
(2)超时实现的方式:一种是手动实现,另一种是通过Redis的过期删除
9、相同点:
存在相同的问题:
a、可能出现,当前持有锁的线程A执行超时,但是此时它仍然是在正常运行的,请求锁的线程B获取到锁,可能会导致业务逻辑出现错误(第二种方法可以通过启动定时任务不断的刷新有效时间来解决)
b、不可重入