● Java 语言连接 redis 服务的方式有: Jedis
、SpringData Redis
、Lettuce
我们这里使用 Jedis,先添加 Jedis 依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.0</version>
</dependency>
// 连接 redis
Jedis jedis = new Jedis("localhost", 6379);
// 操作 redis
jedis.set("name", "itheima");
jedis.get("name");
// 关闭连接
jedis.close();
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();
}
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 节点更新后,也能得到最新的。
最后我们来看一下使用 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 操作框架,只需要一个 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");
我们接着来看看事务操作,由于 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();
}
}
我们之前在学习 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 服务器获取到锁,那么就真的获取到锁了,这样就算挂掉一部分节点,也能保证正常运行,这里就不做演示了。
我们在学习 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 缓存数据了。
我们之前使用 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,启动服务器验证一下吧。
Redis Desktop Manager
官网下载:https://redisdesktop.com/download
总结:
提示:这里对文章进行总结:
本文是对Redis的学习,学习了Jedis(Java与Redis交互的方式),在springboot中使用StringRedisTemplate操作redis,redssion分布式锁的应用,以及redis做mybatis的二级缓存、持久化token等应用。之后的学习内容将持续更新!!!