Redis6.0.6_03_Redis 实用

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类型结构
RedisRedisTemplate rt
set key valuert.opsForValue().set(“key”,“value”)
get keyrt.opsForValue().get(“key”)
del keyrt.delete(“key”)
strlen keyrt.opsForValue().size(“key”)
getset key valuert.opsForValue().getAndSet(“key”,“value”)
getrange key start endrt.opsForValue().get(“key”,start,end)
append key valuert.opsForValue().append(“key”,“value”)
Hash结构
hmset key field1 value1 field2 value2…rt.opsForHash().putAll(“key”,map) //map是一个集合对象
hset key field valuert.opsForHash().put(“key”,“field”,“value”)
hexists key fieldrt.opsForHash().hasKey(“key”,“field”)
hgetall keyrt.opsForHash().entries(“key”) //返回Map对象
hvals keyrt.opsForHash().values(“key”) //返回List对象
hkeys keyrt.opsForHash().keys(“key”) //返回List对象
hmget key field1 field2…rt.opsForHash().multiGet(“key”,keyList)
hsetnx key field valuert.opsForHash().putIfAbsent(“key”,“field”,“value”
hdel key field1 field2rt.opsForHash().delete(“key”,“field1”,“field2”)
hget key fieldrt.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 indexrt.opsForList().index(“list”, index)
llen keyrt.opsForList().size(“key”)
lpop keyrt.opsForList().leftPop(“key”)
rpop keyrt.opsForList().rightPop(“key”)
lpushx list nodert.opsForList().leftPushIfPresent(“list”,“node”)
rpushx list nodert.opsForList().rightPushIfPresent(“list”,“node”)
lrange list start endrt.opsForList().range(“list”,start,end)
lrem list count valuert.opsForList().remove(“list”,count,“value”)
lset key index valuert.opsForList().set(“list”,index,“value”)
Set结构
sadd key member1 member2…rt.boundSetOps(“key”).add(“member1”,“member2”,…)
rt.opsForSet().add(“key”, set) //set是一个集合对象
scard keyrt.opsForSet().size(“key”)
sidff key1 key2rt.opsForSet().difference(“key1”,“key2”) //返回一个集合对象
sinter key1 key2rt.opsForSet().intersect(“key1”,“key2”)//同上
sunion key1 key2rt.opsForSet().union(“key1”,“key2”)//同上
sdiffstore des key1 key2rt.opsForSet().differenceAndStore(“key1”,“key2”,“des”)
sinter des key1 key2rt.opsForSet().intersectAndStore(“key1”,“key2”,“des”)
sunionstore des key1 key2rt.opsForSet().unionAndStore(“key1”,“key2”,“des”)
sismember key memberrt.opsForSet().isMember(“key”,“member”)
smembers keyrt.opsForSet().members(“key”)
spop keyrt.opsForSet().pop(“key”)
srandmember key countrt.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 开发须知

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值