redis支持哪些语言可以操作
使用jedis
(1)添加jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>
(2)代码测试
public class TestJedis {
@Test
//测试redis连接
public void test01(){
//String host, int port redis服务器的ip和端口号
Jedis jedis = new Jedis("192.168.100.130",6379);
//字符串操作
System.out.println("=============字符串操作==================");
String set = jedis.set("k1", "v1");
System.out.println(set);
String k1 = jedis.get("k1");
System.out.println(k1);
Long k11 = jedis.expire("k1", 30l);
System.out.println(k11);
Boolean k12 = jedis.exists("k1");
System.out.println(k12);
String mset = jedis.mset("key1", "value1", "key2", "value2");
System.out.println(mset);
Set<String> keys = jedis.keys("*");
System.out.println(keys);
System.out.println("=============list操作==============");
Long k2 = jedis.lpush("k2", "1", "2", "3");
System.out.println(k2);
List<String> k21 = jedis.lrange("k2", 0, -1);
System.out.println(k21);
}
}
使用连接池连接redis
@Test
public void testPool(){
//this(poolConfig, (String)host, 6379);
//创建连接池配置类
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(3);
jedisPoolConfig.setMaxWaitMillis(3000);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,"192.168.100.130",6379);
Jedis resource = jedisPool.getResource();
List<String> k2 = resource.lrange("k2", 1, -1);
System.out.println(k2);
}
比较jedis和jedisPool的效率
@Test
public void compare(){
Jedis jedis = new Jedis("192.168.100.130",6379);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
jedis.ping();
jedis.close();
}
long end = System.currentTimeMillis();
System.out.println("不使用连接池:"+(end-start));
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(3);
jedisPoolConfig.setMaxWaitMillis(3000);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,"192.168.100.130",6379);
long start1 = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
//从连接池获取资源
Jedis resource = jedisPool.getResource();
resource.ping();
//释放资源到连接池
resource.close();
}
long end1 = System.currentTimeMillis();
System.out.println("使用连接池:"+(end1-start1));
}
使用连接池比不使用连接池的效率高三倍。
Java连接redis集群模式
开启redis集群
连接Windows可视化工具redis plus
注意一定要放行集群中所有的端口或者关闭防火墙
firewall-cmd --add-port=6001/tcp --zone=public --permanent
systemctl restart firewalld
测试
@Test
public void testCluster(){
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.100.130",6001));
nodes.add(new HostAndPort("192.168.100.130",6002));
nodes.add(new HostAndPort("192.168.100.130",6003));
nodes.add(new HostAndPort("192.168.100.130",6004));
nodes.add(new HostAndPort("192.168.100.130",6005));
nodes.add(new HostAndPort("192.168.100.130",6006));
JedisCluster jedisCluster = new JedisCluster(nodes);
Long hset = jedisCluster.hset("dumpling", "name", "dumpling");
System.out.println(hset);
}
springboot整合redis
springboot对redis的操作封装了两个StringRedisTemplate和RedisTemplate类,StringRedisTemplate是RedisTemplate的子类,StringRedisTemplate它只能存储字符串类型,无法存储对象类型。要想用StringRedisTemplate存储对象必须把对象转为json字符串。
1、StringRedisTemplate
(1)引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)编写配置文件
# redis配置
spring.redis.host=192.168.100.130
spring.redis.port=6380
(3)注入StringRedisTemplate
@Autowired
private StringRedisTemplate stringRedisTemplate;
(4)使用StringRedisTemplate
该类把对每种数据类型的操作,单独封装了相应的内部类。
存储字符串类型数据
@SpringBootTest
class Jedis02ApplicationTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testString(){
//获取所有的key
stringRedisTemplate.keys("*");
//是否存在指定的key
stringRedisTemplate.hasKey("key");
//删除指定的key
stringRedisTemplate.delete("key");
//对字符串进行操作
ValueOperations<String, String> forValue = stringRedisTemplate.opsForValue();
//存入字符串类型的value并设置过期时间
forValue.set("姓名","张三",24, TimeUnit.HOURS);
//相当于setnx(),存入成功则返回true,存入失败则返回false
Boolean aBoolean = forValue.setIfAbsent("姓名", "李四");
System.out.println(aBoolean);
//在指定key的value值追加指定内容,返回值为value值的大小
Integer append = forValue.append("姓名", "大好人");
System.out.println(append);
}
}
存储hash类型的数据
在StringRedisTemplate类中所有的属性都必须为String类型
@SpringBootTest
class Jedis02ApplicationTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testHash(){
//对hash类型操作
HashOperations<String, Object, Object> forHash = stringRedisTemplate.opsForHash();
forHash.put("hash1","name","dumpling");
forHash.put("hash1","age","18");
Map<String,String> map = new HashMap<>();
map.put("name","skin");
map.put("address","China");
forHash.putAll("hash2",map);
//获取指定key的所有内容
Map<Object, Object> hash2 = forHash.entries("hash2");
System.out.println(hash2);
//获取指定key的指定field的value值
Object o = forHash.get("hash1", "name");
System.out.println(o);
//获取指定key的所有field值,返回类型为set,因为field值没有重复的
Set<Object> hash1 = forHash.keys("hash1");
System.out.println(hash1);
//获取指定key的所有value值,返回类型为list,因为value值的内容有重复的
List<Object> hash11 = forHash.values("hash1");
System.out.println(hash11);
}
}
2、RedisTemplate
RedisTemplate类默认的采用jdk自带的序列化方式,我们在使用的时候会发现在redis数据库中会有乱码现象,我们需要手动指定序列化方式。
@SpringBootTest
class Jedis02ApplicationTests02 {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test(){
//为field设置序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
//为value设置序列化方式
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("String1","dumpling");
Object o = valueOperations.get("String1");
System.out.println(o);
valueOperations.set("user",new User("dumpling",23));
}
}
上面的RedisTemplate需要每次都指定key value以及field的序列化方式,我们可以写一个配置类,为RedisTemplate指定好序列化。以后再用就无需指定。
redis的使用场景
1、作为缓存
数据存储在内存中,数据查询速度快。可以分摊数据库压力。
适合放入缓存的数据:
查询频率比较高,修改的频率比较低
安全系数低的数据
使用redis作为缓存
这里使用mybatis-plus来做演示
(1)引入相关依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
(2)编写配置文件
# redis配置
spring.redis.host=192.168.100.130
spring.redis.port=63780
# mysql配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=990412
spring.datasource.url=jdbc:mysql://localhost:3306/stu?serverTimezone=Asia/Shanghai
# 日志设置
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
(3)测试
@Service
public class StuService {
@Autowired
private StuMapper stuMapper;
@Autowired
RedisTemplate redisTemplate;
public Stu findById(Integer id){
ValueOperations valueOperations = redisTemplate.opsForValue();
Object o = valueOperations.get("stu::" + id);
if (o!=null){
return (Stu) o;
}
Stu byId = stuMapper.selectById(id);
if (byId!=null){
valueOperations.set("stu::"+id,byId,24, TimeUnit.HOURS);
}
System.out.println(byId);
return byId;
}
public Integer insert(Stu stu){
int insert = stuMapper.insert(stu);
return insert;
}
public Integer delete(Integer id){
redisTemplate.delete("stu::"+id);
int i = stuMapper.deleteById(id);
return i;
}
public Stu update(Stu stu){
redisTemplate.delete("stu::"+stu.getId());
stuMapper.updateById(stu);
return stu;
}
}
我们在查询时从缓存中查找和从数据库查找之后存入缓存的业务: 前部分代码等同于@before通知,后部分代码等同于后置通知。 我们可以AOP完成缓存代码和业务代码分离。
spring框架帮我们提供了相应的注解。解析该注解就可以达到我们的目的。
(1)加入缓存的配置类
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer 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);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
(2)开启注解
(3) 使用注解
@Service
public class StuService {
@Autowired
private StuMapper stuMapper;
//查询注解 cacheNames表示缓存的名称 key:唯一标志---stu::key
// 先从缓存中查看key为(cacheNames::key)的是否存在
// 如果存在则不会执行方法体,如果不存在则执行方法体并把方法的返回值存入缓存中
@Cacheable(cacheNames = {"stu"},key = "#id")
public Stu findById(Integer id){
Stu byId = stuMapper.selectById(id);
return byId;
}
public Integer insert(Stu stu){
int insert = stuMapper.insert(stu);
return insert;
}
//先删除缓存,在执行方法体
@CacheEvict(cacheNames = "stu",key = "#id")
public Integer delete(Integer id){
int i = stuMapper.deleteById(id);
return i;
}
//这个注解可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存和数据库的同步更新
@CachePut(cacheNames = "stu",key = "#stu.id")
public Stu update(Stu stu){
stuMapper.updateById(stu);
return stu;
}
}
2、分布式锁
使用压测工具测试高并发下带来线程安全问题
我们可以发现同一个库存被使用了n次,数据库中也出现了负数,这是线程安全导致的。
解决方案:
1、使用synchronized或lock锁
@Service public class ProductStockServiceImpl2 implements ProductStockService { @Autowired private ProductStockDao productStockDao; @Override public String decreaseStock(Integer productId) { synchronized (this){ //查看该商品的库存数量 Integer stock = productStockDao.findStockByProductId(productId); if (stock > 0) { //修改库存每次-1 productStockDao.updateStockByProductId(productId); System.out.println("扣减成功!剩余库存数:" + (stock - 1)); return "success"; } else { System.out.println("扣减失败!库存不足!"); return "fail"; } } } }
使用synchronized 或者lock锁 如果我们搭建了项目集群,那么该锁无效。
测试:使用idea开启集群项目
我们发现运行结果中仍然有重复的,数据库中的值依然变成负数了
我们使用redis中存储key来作为锁。
@Service
public class ProductStockServiceImpl2 implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
public String decreaseStock(Integer productId) {
ValueOperations valueOperations = redisTemplate.opsForValue();
Boolean aBoolean = valueOperations.setIfAbsent("productId::" + productId, "成功");
if (aBoolean){
try {
//查看该商品的库存数量
Integer stock = productStockDao.findStockByProductId(productId);
if (stock > 0) {
//修改库存每次-1
productStockDao.updateStockByProductId(productId);
System.out.println("扣减成功!剩余库存数:" + (stock - 1));
return "success";
} else {
System.out.println("扣减失败!库存不足!");
return "fail";
}
} finally {
redisTemplate.delete("productId::" + productId);
}
}
return "系统正忙~~~~~~~~~~";
}
}
Redis分布式锁的缺陷
Redis分布式锁主要依靠Redis服务来完成,我们的应用程序其实是Redis节点的客户端,一旦客户端没有释放锁,服务端就会一直持有这个锁,其他进程中的线程就无法获取到这把锁,于是就会发生锁死的情况。所以我们在使用Redis分布式锁的时候,务必要设置锁的过期时间。
主要基于下面两点:
网络抖动
客户端A中的一个线程获取到了锁,然后执行finally中的释放锁的代码时,这时候网络出问题了,导致客户端A没有成功释放锁。此时对于redis服务端来说,它会一直把锁给客户端A,这样的话其他客户端自然也就不能获取到这个锁。 如果是设置了过期时间的话,即使客户端和服务端的网络不通了,服务端依然在进行时间的计算,时间到了直接把锁释放掉,等网络通了,不影响其他客户端获取锁。
Redis宕机
客户端A获取到了锁,Redis服务器突然宕机,锁没有释放。等到Redis再次恢复的时候,Redis服务端还会把锁给到客户端A,这样也会发生锁死的情况。
如果是设置了过期时间的话,服务器恢复后就会继续倒计时,时间到了服务器自动把锁释放,其他客户端也就可以尝试去获取锁了
Redis分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
解决方法:可以使用redission依赖解决在这个问题。
解决超时问题的原理:
为持有锁的线程开启一个守护线程,守护线程会每隔10秒检查当前线程是否还持有锁,如果持有则延迟生存时间。
(1)引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
(2)将Redission交与spring容器管理
@SpringBootApplication
public class DistrinctLockApplication {
public static void main(String[] args) {
SpringApplication.run(DistrinctLockApplication.class, args);
}
//获取redisson对象并交于spring容器管理
@Bean //setnx()
public Redisson redisson(){
Config config =new Config();
config.useSingleServer().
setAddress("redis://192.168.100.130:6379").
//redis默认有16个数据库
setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
使用redission对象
@Service
public class ProductStockServiceImpl2 implements ProductStockService {
@Autowired
private ProductStockDao productStockDao;
@Autowired
private Redisson redisson;
@Override
public String decreaseStock(Integer productId) {
//获取锁
RLock lock = redisson.getLock("product::" + productId);
try {
//加锁
lock.lock(10,TimeUnit.SECONDS);
//查看该商品的库存数量
Integer stock = productStockDao.findStockByProductId(productId);
if (stock > 0) {
//修改库存每次-1
productStockDao.updateStockByProductId(productId);
System.out.println("扣减成功!剩余库存数:" + (stock - 1));
return "success";
} else {
System.out.println("扣减失败!库存不足!");
return "fail";
}
} finally {
//释放锁
lock.unlock();
}
}
}