Redis在Java中的应用

发布时间:2024-01-11 19:32:21


提示:以下是本篇文章正文内容,Redis 系列学习将会持续更新

一、Jedis: Java 与 Redis 交互

在这里插入图片描述
Java 语言连接 redis 服务的方式有: JedisSpringData RedisLettuce

我们这里使用 Jedis,先添加 Jedis 依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.0.0</version>
</dependency>

🍉1.1 Jedis 连接 redis

// 连接 redis
Jedis jedis = new Jedis("localhost", 6379);

// 操作 redis
jedis.set("name", "itheima");
jedis.get("name");

// 关闭连接
jedis.close();

🍉1.2 JedisPool 连接池

JedisPool:Jedis 提供的连接池技术。

// poolConfig: 连接池配置对象
// host: redis服务地址
// port: redis服务端口号
public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) {
    this(poolConfig, host, port, 2000, (String)null, 0, (String)null);
}

①封装连接参数: redis.properties

redis.host=127.0.0.1
redis.port=6379
redis.maxTotal=30
redis.maxIdle=10

②定义连接池:

public class JedisUtils {

    private static JedisPoolConfig jpc;
    private static JedisPool jp;
    private static String host;
    private static int port;
    private static int maxTotal;
    private static int maxIdle;

	// 3-2 加载配置信息:
    // 静态代码块初始化资源
    static {
        // 读取配置文件, 获得参数值
        ResourceBundle rb = ResourceBundle.getBundle("redis");
        host = rb.getString("redis.host");
        port = Integer.parseInt(rb.getString("redis.port"));
        maxTotal = Integer.parseInt(rb.getString("redis.maxTotal"));
        maxIdle = Integer.parseInt(rb.getString("redis.maxIdle"));

        jpc = new JedisPoolConfig();
        jpc.setMaxTotal(maxTotal);
        jpc.setMaxIdle(maxIdle);
        jp = new JedisPool(jpc, host, port);
    }

    // 3-3 获取连接:
    // 对外访问接口,提供jedis连接对象,连接从连接池获取
    public static Jedis getJedis() {
        return jp.getResource();
    }
}

③使用连接池:

public static void main(String[] args) {
    Jedis jedis = JedisUtils.getJedis();
    jedis.set("name", "zhangsan");
    jedis.close();
}

🍉1.3 JedisSentinelPool 获取 master

public class Main {
    public static void main(String[] args) {
        // 这里我们直接使用 JedisSentinelPool 来获取 Master 节点
        // 需要把三个哨兵的地址都填入
        try (JedisSentinelPool pool = new JedisSentinelPool("lbwnb",
                new HashSet<>(Arrays.asList("127.0.0.1:26380", "127.0.0.1:26381", "127.0.0.1:26382")))) {
            Jedis jedis = pool.getResource();   // 直接询问并得到Jedis对象,这就是连接的Master节点
            jedis.set("test", "114514");    // 直接写入即可,实际上就是向 Master 节点写入

            Jedis jedis2 = pool.getResource();   // 再次获取
            System.out.println(jedis2.get("test"));   // 读取操作
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这样,Jedis 对象就可以通过哨兵来获取,当 Master 节点更新后,也能得到最新的。

🍉1.4 JedisCluster 操作 redis 集群

最后我们来看一下使用 Java 连接到集群模式下的 Redis,我们需要用到 JedisCluster 对象:

public class Main {
    public static void main(String[] args) {
        // 和客户端一样,随便连一个就行,也可以多写几个,构造方法有很多种可以选择
        try(JedisCluster cluster = new JedisCluster(new HostAndPort("127.0.0.1", 6379))){
            System.out.println("集群实例数量:" + cluster.getClusterNodes().size());
            cluster.set("a", "yyds");
            System.out.println(cluster.get("a"));
        }
    }
}

回到目录…

二、SpringBoot 整合 Redis

🍆2.1 StringRedisTemplate 操作

①我们接着来看如何在 SpringBoot 项目中整合 Redis 操作框架,只需要一个 starter 即可,但是它底层没有用 Jedis,而是 Lettuce

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

②starter 提供的默认配置会去连接本地的 Redis 服务器,并使用 0号数据库,当然你也可以手动进行修改:

spring:
  redis:
    host: 192.168.10.3
    port: 6379
    database: 0

③我们可以直接注入StringRedisTemplate 来操作 Redis:

@Service
public class RedisService {
    @Resource
    private StringRedisTemplate template;

    public void test() {
        // 操作 string 类型
        template.opsForValue().set("name", "zhangsan");
        System.out.println(template.opsForValue().get("name"));
        template.delete("c");    //删除键
        System.out.println(template.hasKey("c"));   //判断是否包含键
        // 操作 hash 类型
        template.opsForHash().put("myhash", "name", "lisi");
        template.opsForHash().put("myhash", "age", "20");
    }
}

实际上所有的值的操作都被封装到了 ValueOperations 对象中,而普通的键操作直接通过模板对象就可以使用了,大致使用方式其实和 Jedis 一致。

ValueOperations<String, String> operations = template.opsForValue();
operations.set("c", "xxxxx");

回到目录…

🍆2.2 Redis 事务

我们接着来看看事务操作,由于 Spring 没有专门的 Redis 事务管理器,所以只能借用 JDBC 提供的,只不过无所谓,正常情况下反正我们也要用到这玩意:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

开启事务:我们控制层直接调用它的执行方法 start() 就可以开启事务

@Service
public class RedisTransactionService {
    @Resource
    private StringRedisTemplate template;

    @PostConstruct
    public void init(){
        template.setEnableTransactionSupport(true); // 需要开启事务
    }

    @Transactional // 需要添加此注解
    public void start(){
        template.multi();
        template.opsForValue().set("name", "zhangsan");
        template.opsForValue().set("name", "lisi");
        template.exec();
    }
}

回到目录…

🍆2.3 Redisson 分布式锁

我们之前在学习 SpringCloud Alibaba之 Seata与分布式事务 时,提到了下面的问题:

@Override
public boolean doBorrow(int uid, int bid) {
  	//1. 判断图书和用户是否都支持借阅,如果此时来了10个线程,都进来了,那么都能够判断为可以借阅
    if(bookClient.bookRemain(bid) < 1)
        throw new RuntimeException("图书数量不足");
    if(userClient.userRemain(uid) < 1)
        throw new RuntimeException("用户借阅量不足");
  	//2. 首先将图书的数量-1,由于上面10个线程同时进来,同时判断可以借阅,那么这个10个线程就同时将图书数量-1,那库存岂不是直接变成负数了???
    if(!bookClient.bookBorrow(bid))
        throw new RuntimeException("在借阅图书时出现错误!");
  	...
}

实际上在高并发下,我们看似正常的借阅流程,会出现问题,比如现在同时来了10个同学要借同一本书,但是现在只有3本,而我们的判断规则是,首先看书够不够,如果此时这10个请求都已经走到这里,并且都判定为可以进行借阅,那么问题就出现了,接下来这10个请求都开始进行借阅操作,导致库存直接爆表,形成超借问题(在电商系统中也存在同样的超卖问题

因此,为了解决这种问题,我们就可以利用分布式锁来实现。那么Redis实现分布式锁可以参考往期文章Redis 事务和锁

但是如果使用 setnx key value 可能导致死锁。而使用 set key value EX seconds NX 又会出现新的问题:事务没执行完锁就过期了、锁过期但自己不知道就删了别人的锁。

要解决这个问题,我们可以借助一下 Redisson 框架,它是 Redis 官方推荐的 Java 版的 Redis 客户端。它提供的功能非常多,也非常强大,Redisson 内部提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期,它为我们提供了很多种分布式锁的实现,使用起来也类似我们在 JUC 中学习的锁,这里我们尝试使用一下它的分布式锁功能。

①添加依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.17.0</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.75.Final</version>
</dependency>

②首先我们来看看不加锁的情况下:

public static void main(String[] args) {
    // 执行前,先向 redis 中手动 set a 0
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try(Jedis jedis = new Jedis("127.0.0.1", 6379)) {
                for (int j = 0; j < 10; j++) {
                    int a = Integer.parseInt(jedis.get("a")) + 1;
                    String a2 = Integer.toString(a);
                    jedis.set("a", a2);
                }
            }
        }).start();
    }
}

预期结果:100;实际结果:17;误差很大。

③现在我们来给它加一把锁,注意这个锁是基于Redis的,不仅仅只可以用于当前应用,是能够跨系统的:

public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://192.168.0.10:6379");   //配置连接的Redis服务器,也可以指定集群
    RedissonClient client =  Redisson.create(config);   //创建RedissonClient客户端
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try(Jedis jedis = new Jedis("192.168.0.10", 6379)){
                RLock lock = client.getLock("testLock");    //指定锁的名称,拿到锁对象
                for (int j = 0; j < 100; j++) {
                    lock.lock();    //加锁
                    int a = Integer.parseInt(jedis.get("a")) + 1;
                    jedis.set("a", a+"");
                    lock.unlock();   //解锁
                }
            }
            System.out.println("结束!");
        }).start();
    }
}

预期结果:100;实际结果:100;没有误差,过程中我们也发现 redis 库中有了 testLock 锁字段。

注意:如果用于存放锁的 Redis 服务器挂了,那么肯定是会出问题的,这个时候我们就可以使用 RedLock,它的思路是,在多个 Redis 服务器上保存锁,只需要超过半数的 Redis 服务器获取到锁,那么就真的获取到锁了,这样就算挂掉一部分节点,也能保证正常运行,这里就不做演示了。

回到目录…

三、Redis 应用

🌽3.1 MyBatis 二级缓存

我们在学习 Mybatis 时介绍了它的二级缓存,它是 Mapper 级别的缓存,能够作用与所有会话。但是当时我们提出了一个问题,由于 Mybatis 的默认二级缓存只能是单机的,如果存在多台服务器访问同一个数据库,实际上二级缓存只会在各自的服务器上生效,但是我们希望的是多台服务器都能使用同一个二级缓存,这样就不会造成过多的资源浪费。
在这里插入图片描述
我们可以将 Redis 作为 Mybatis 的二级缓存,这样就能实现多台服务器使用同一个二级缓存,因为它们只需要连接同一个 Redis 服务器即可,所有的缓存数据全部存储在 Redis 服务器上。

①我们需要手动实现 Mybatis 提供的 Cache 接口,这里我们简单编写一下:

// 实现 Mybatis 的 org.apache.ibatis.cache.Cache 接口
public class RedisMybatisCache implements Cache {

    private final String id;
    private static RedisTemplate<Object, Object> template;

   	// 注意构造方法必须带一个String类型的参数接收id
    public RedisMybatisCache(String id){
        this.id = id;
    }

  	// 初始化时通过配置类将RedisTemplate给过来
    public static void setTemplate(RedisTemplate<Object, Object> template) {
        RedisMybatisCache.template = template;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object o, Object o1) {
      	// 这里直接向Redis数据库中丢数据即可,o就是Key,o1就是Value,60秒为过期时间
        template.opsForValue().set(o, o1, 60, TimeUnit.SECONDS);
    }

    @Override
    public Object getObject(Object o) {
      	// 这里根据Key直接从Redis数据库中获取值即可
        return template.opsForValue().get(o);
    }

    @Override
    public Object removeObject(Object o) {
      	//根据Key删除
        return template.delete(o);
    }

    @Override
    public void clear() {
      	// 由于template中没封装清除操作,只能通过connection来执行
		template.execute((RedisCallback<Void>) connection -> {
          	// 通过connection对象执行清空操作
            connection.flushDb();
            return null;
        });
    }

    @Override
    public int getSize() {
      	// 这里也是使用connection对象来获取当前的Key数量
        return template.execute(RedisServerCommands::dbSize).intValue();
    }
}

②缓存类编写完成后,我们接着来编写配置类:

@Configuration
public class MyBatisConfiguration {
    @Resource
    RedisTemplate<Object, Object> template;

    @PostConstruct
    public void init(){
        // 把 RedisTemplate 给到 RedisMybatisCache
        RedisMybatisCache.setTemplate(template);
    }
}

③最后我们在 Mapper 上启用此缓存即可:

@Mapper
// 只需要修改缓存实现类为我们的 RedisMybatisCache 即可
@CacheNamespace(implementation = RedisMybatisCache.class)
public interface BookMapper {
    @Select("select * from book where bid = #{bid}")
    Book getBookById(int bid);
}

④我们的实体类必须实现 Serializable 序列化接口

@Data
public class Book implements Serializable {
    private int bid;
    private String title;
    private String desc;
}

⑤最后我们调用接口查看当前的二级缓存是否生效:手动查看Redis数据库,可以看到一条 Mybatis 缓存数据了。
在这里插入图片描述

回到目录…

🌽3.2 持久化 Token

我们之前使用 SpringSecurity 时,remember-me 的 Token 是支持持久化存储的,而我们当时是存储在数据库中,那么 Token 信息也是可以存储在缓存中的。

①我们先手动实现一个 PersistentTokenRepository 接口

@Component
public class RedisTokenRepository implements PersistentTokenRepository {
  	//Key名称前缀,用于区分
    private final static String REMEMBER_ME_KEY = "spring:security:rememberMe:";
    @Resource
    private RedisTemplate<Object, Object> template;

    @Override
    public void createNewToken(PersistentRememberMeToken token) {
      	//这里要放两个,一个存seriesId->Token,一个存username->seriesId,因为删除时是通过username删除
        template.opsForValue().set(REMEMBER_ME_KEY+"username:"+token.getUsername(), token.getSeries());
        template.expire(REMEMBER_ME_KEY+"username:"+token.getUsername(), 1, TimeUnit.DAYS);
        this.setToken(token);
    }

  	//先获取,然后修改创建一个新的,再放入
    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        PersistentRememberMeToken token = this.getToken(series);
        if(token != null)
           this.setToken(new PersistentRememberMeToken(token.getUsername(), series, tokenValue, lastUsed));
    }

    @Override
    public PersistentRememberMeToken getTokenForSeries(String seriesId) {
        return this.getToken(seriesId);
    }

  	//通过username找seriesId直接删除这两个
    @Override
    public void removeUserTokens(String username) {
        String series = (String) template.opsForValue().get(REMEMBER_ME_KEY+"username:"+username);
        template.delete(REMEMBER_ME_KEY+series);
        template.delete(REMEMBER_ME_KEY+"username:"+username);
    }

  
  	//由于PersistentRememberMeToken没实现序列化接口,这里只能用Hash来存储了,所以单独编写一个set和get操作
    private PersistentRememberMeToken getToken(String series){
        Map<Object, Object> map = template.opsForHash().entries(REMEMBER_ME_KEY+series);
        if(map.isEmpty()) return null;
        return new PersistentRememberMeToken(
                (String) map.get("username"),
                (String) map.get("series"),
                (String) map.get("tokenValue"),
                new Date(Long.parseLong((String) map.get("date"))));
    }

    private void setToken(PersistentRememberMeToken token){
        Map<String, String> map = new HashMap<>();
        map.put("username", token.getUsername());
        map.put("series", token.getSeries());
        map.put("tokenValue", token.getTokenValue());
        map.put("date", ""+token.getDate().getTime());
        template.opsForHash().putAll(REMEMBER_ME_KEY+token.getSeries(), map);
        template.expire(REMEMBER_ME_KEY+token.getSeries(), 1, TimeUnit.DAYS);
    }
}

②接着把验证 Service 实现了:

@Service
public class AuthService implements UserDetailsService {

    @Resource
    private UserMapper mapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = mapper.getAccountByUsername(username);
        if(account == null) throw new UsernameNotFoundException("");
        return User
                .withUsername(username)
                .password(account.getPassword())
                .roles(account.getRole())
                .build();
    }
}

③Mapper 也安排上:

@Data
public class Account implements Serializable {
    int id;
    String username;
    String password;
    String role;
}
@CacheNamespace(implementation = MybatisRedisCache.class)
@Mapper
public interface UserMapper {

    @Select("select * from users where username = #{username}")
    Account getAccountByUsername(String username);
}

④最后配置文件配一波:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Resource
	private RedisTokenRepository repository;
	@Resource
	private AuthService service;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                .tokenRepository(repository);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(service)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

OK,启动服务器验证一下吧。

回到目录…

四、windows 下的可视化客户端

Redis Desktop Manager
在这里插入图片描述

官网下载:https://redisdesktop.com/download

回到目录…


总结:
提示:这里对文章进行总结:
本文是对Redis的学习,学习了Jedis(Java与Redis交互的方式),在springboot中使用StringRedisTemplate操作redis,redssion分布式锁的应用,以及redis做mybatis的二级缓存、持久化token等应用。之后的学习内容将持续更新!!!

文章来源:https://blog.csdn.net/qq15035899256/article/details/128507965
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。