一.redis的发布订阅
什么 是发布和订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
Redis的发布和订阅
客户端订阅频道发布的消息
频道发布消息 订阅者就可以收到消息
发布订阅的代码实现
1、 打开一个客户端订阅channel1
SUBSCRIBE channel1
2、打开另一个客户端,给channel1发布消息hello
publish channel1 hello
返回的1是订阅者数量
3、打开第一个客户端可以看到发送的消息
二.Redis事务
1.事务简介:
可以一次执行多个命令,本质是一组命令的集合。一个事务中的 所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞。
单独的隔离的操作
官网说明
https://redis.io/docs/interact/transactions/
MULTI、EXEC、DISCARD、WATCH。这四个指令构成了 redis 事务处理的基础。
1.MULTI 用来组装一个事务;将命令存放到一个队列里面
2.EXEC 用来执行一个事务;//commit
3.DISCARD 用来取消一个事务;//rollback
4.WATCH 用来监视一些 key,一旦这些 key 在事务执行之前被改变,则取消事务的执行。
例子:
redis> MULTI //标记事务开始
OK
redis> INCR user_id //多条命令按顺序入队
QUEUED queued
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC //执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
有关事务,经常会遇到的是两类错误:
1.调用 EXEC 之前的错误
“调用 EXEC 之前的错误”,有可能是由于语法有误导致的,也可能时由于内存不足导致的。只要出现某个命令无法成功写入缓冲队列的情况,redis 都会进行记录,在客户端调用 EXEC 时,redis 会拒绝执行这一事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> haha //一个错误的指令
(error) ERR unknown command 'haha'
127.0.0.1:6379> ping
QUEUED
127.0.0.1:6379> exec
//redis 拒绝了事务的执行,原因是“之前出现了错误”
(error) EXECABORT Transaction discarded because of previous errors.
2.调用 EXEC 之后的错误
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 23
QUEUED
//age 不是集合,所以如下是一条明显错误的指令
127.0.0.1:6379> sadd age 15
QUEUED
127.0.0.1:6379> set age 29
QUEUED
127.0.0.1:6379> exec //执行事务时,redis 不会理睬第 2 条指令执行错误
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get age
"29" //可以看出第 3 条指令被成功执行了
2.redis事务冲突
双十一去购物的时候使用同一张银行卡去付款
10000
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
解决方案
悲观锁
select * from biao where 1=1 for update;
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,
这样别人想拿这个数据就会block直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,
比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
12306抢票
乐观锁
version 1
查余额 10000 version:1
10000>8000 -8000 update uuuu set moner-=8000 where version=1 1.1
10000 -5000 UPDATE uuuuu SET MONTY-=5000 WHERE VERSION=1
2000 2000>1000 UPDATE uuuu SET MONTY-=1000 WHERE VERSION=1.1 1.2
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,
每次去拿数据的时候都认为别人不会修改,所以不会上锁,
但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
可以使用版本号等机制。乐观锁适用于多读的应用类型,
这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
3.WATCH
“WATCH”可以帮我们实现类似于“乐观锁”的效果,即 CAS(check and set)。
WATCH 本身的作用是“监视 key 是否被改动过”,而且支持同时监视多个 key,只要还没真正触发事务,WATCH 都会尽职尽责的监视,一旦发现某个 key 被修改了,在执行 EXEC 时就会返回 nil,表示事务无法触发。
代码如下:
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> watch age //开始监视 age
OK
127.0.0.1:6379> set age 24 //在 EXEC 之前,age 的值被修改了
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 25
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> exec //触发 EXEC
(nil) //事务无法被执行
事务回滚:
192.168.109.34:6379> multi
OK
192.168.109.34:6379> set testsy testq
QUEUED
192.168.109.34:6379> set testsy2 test2
QUEUED
192.168.109.34:6379> discard
OK
三.Redis的使用
java操作redis
创建java项目
添加redis的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
相关API
key的api
@Test
public void testRedis() {
//设置连接的服务器 端口号默认是6379
// 服务器的默认值是localhost
Jedis jedis=new Jedis("8.140.27.154");
// redis的服务器的密码
jedis.auth("lh"); //设置密码
// 设置选中的数据库的下标
jedis.select(15);
// 设置键值对
jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
//获取所有的key值
Set<String> keys = jedis.keys("*");
System.out.println(keys.size());
for (String key : keys) {
System.out.println(key);
}
// 判断key是否存在
System.out.println(jedis.exists("k1"));
//获取key的过期时间
System.out.println(jedis.ttl("k1"));
// 获取key对应的值
System.out.println(jedis.get("k1"));
}
}
string-api
// 批量设置
jedis.mset("str1","v1","str2","v2","str3","v3");
//批量获取key
System.out.println(jedis.mget("str1","str2","str3"));
hash-api
//设置 一个key叫做hash1 对应的field是username,value是lisi
jedis.hset("hash1","userName","lisi");
//获取key为hash1的对应的fileld为username的值
System.out.println(jedis.hget("hash1","userName"));
Map<String,String> map = new HashMap<String,String>();
map.put("telphone","13838389438");
map.put("address","郑州");
map.put("email","abc@163.com");
//批量设置
jedis.hmset("hash2",map);
//批量获取
List<String> result = jedis.hmget("hash2", "telphone","email");
for (String element : result) {
System.out.println(element);
}
set-api
jedis.sadd("orders", "order01");
jedis.sadd("orders", "order02");
jedis.sadd("orders", "order03");
jedis.sadd("orders", "order04");
Set<String> smembers = jedis.smembers("orders");
for (String order : smembers) {
System.out.println(order);
}
//删除集合中的元素
jedis.srem("orders", "order02");
zset-api
jedis.zadd("zset01", 100d, "z3");
jedis.zadd("zset01", 90d, "l4");
jedis.zadd("zset01", 80d, "w5");
jedis.zadd("zset01", 70d, "z6");
Set<String> zrange = jedis.zrange("zset01", 0, -1);
for (String e : zrange) {
System.out.println(e);
}
list-api
//添加
jedis.lpush("mylist","test1","test2","test3");
//获取list里面的值
List<String> mylist = jedis.lrange("mylist", 0, -1);
for (String s : mylist) {
System.out.println(s);
}
redis整合springboot
创建springboot项目
过程略
加入redis的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
编写配置文件
#设置reis的索引
spring.redis.database=15
#设置连接redis的密码
spring.redis.password=yyl
#设置的redis的服务器
spring.redis.host=192.168.159.34
#端口号
spring.redis.port=6379
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
设置配置类
package com.example.demo;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* 连接池的设置
*
* @return
*/
@Bean
public JedisPoolConfig getJedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
return jedisPoolConfig;
}
/**
* RedisTemplate
* @param factory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
/**
* 缓存处理
* @param factory
* @return
*/
@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))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
编写测试代码
spring-data-redis针对jedis提供了如下功能:
1. 连接池自动管理,提供了一个高度封装的“RedisTemplate”类
2. RedisTemplate 对五种数据结构分别定义了操作
操作字符串
redisTemplate.opsForValue();
操作hash
redisTemplate.opsForHash();
操作list
redisTemplate.opsForList();
操作set
redisTemplate.opsForSet();
操作有序set
redisTemplate.opsForZSet();
3. 针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer)
JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。
StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。
JacksonJsonRedisSerializer:jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-mapper-asl工具支持】
OxmSerializer:提供了将javabean与xml之间的转换能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存储的数据将是xml工具。不过使用此策略,编程将会有些难度,而且效率最低;不建议使用。【需要spring-oxm模块的支持】
String类型常用方法
判断是否有key所对应的值,有则返回true,没有则返回false
redisTemplate.hasKey(key)
有则取出key值所对应的值
redisTemplate.opsForValue().get(key)
redisTemplate.opsForValue().set("key","val")
删除单个key值
redisTemplate.delete(key)
批量删除key
redisTemplate.delete(keys) //其中keys:Collection<K> keys
void set(K key, V value);
redisTemplate.opsForValue().set("num","123");
redisTemplate.opsForValue().get("num") 输出结果为123
void set(K key, V value, long timeout, TimeUnit unit);
redisTemplate.opsForValue().set("num","123",10, TimeUnit.SECONDS);
redisTemplate.opsForValue().get("num")设置的是10秒失效,十秒之内查询有结果,十秒之后返回为null
TimeUnit.DAYS //天
TimeUnit.HOURS //小时
TimeUnit.MINUTES //分钟
TimeUnit.SECONDS //秒
TimeUnit.MILLISECONDS //毫秒
void set(K key, V value, long offset);
覆写(overwrite)给定 key 所储存的字符串值,从偏移量 offset 开始
template.opsForValue().set("key","hello world");
template.opsForValue().set("key","redis", 6);
System.out.println("***************"+template.opsForValue().get("key"));
结果:***************hello redis
V get(Object key);
template.opsForValue().set("key","hello world");
System.out.println("***************"+template.opsForValue().get("key"));
结果:***************hello world
getAndSet V getAndSet(K key, V value);
设置键的字符串值并返回其旧值
template.opsForValue().set("getSetTest","test");
System.out.println(template.opsForValue().getAndSet("getSetTest","test2"));
结果:test
append Integer append(K key, String value);
如果key已经存在并且是一个字符串,则该命令将该值追加到字符串的末尾。如果键不存在,则它被创建并设置为空字符串,因此APPEND在这种特殊情况下将类似于SET。
template.opsForValue().append("test","Hello");
System.out.println(template.opsForValue().get("test"));
template.opsForValue().append("test","world");
System.out.println(template.opsForValue().get("test"));
Hello
Helloworld
size Long size(K key);
返回key所对应的value值得长度
template.opsForValue().set("key","hello world");
System.out.println("***************"+template.opsForValue().size("key"));
***************11
List类型
通过索引获取列表中的元素
redisTemplate.opsForList().index(key, index)
获取列表指定范围内的元素(start开始位置, 0是开始位置,end 结束位置, -1返回所有)
redisTemplate.opsForList().range(key, start, end)
存储在list的头部,即添加一个就把它放在最前面的索引处
redisTemplate.opsForList().leftPush(key, value)
把多个值存入List中(value可以是多个值,也可以是一个Collection value)
redisTemplate.opsForList().leftPushAll(key, value)
List存在的时候再加入
redisTemplate.opsForList().leftPushIfPresent(key, value)
如果pivot处值存在则在pivot前面添加
redisTemplate.opsForList().leftPush(key, pivot, value)
按照先进先出的顺序来添加(value可以是多个值,或者是Collection var2)
redisTemplate.opsForList().rightPush(key, value)
redisTemplate.opsForList().rightPushAll(key, value)
在pivot元素的右边添加值
redisTemplate.opsForList().rightPush(key, pivot, value)
设置指定索引处元素的值
redisTemplate.opsForList().set(key, index, value)
移除并获取列表中第一个元素(如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止)
redisTemplate.opsForList().leftPop(key)
redisTemplate.opsForList().leftPop(key, timeout, unit)
移除并获取列表最后一个元素
redisTemplate.opsForList().rightPop(key)
redisTemplate.opsForList().rightPop(key, timeout, unit)
从一个队列的右边弹出一个元素并将这个元素放入另一个指定队列的最左边
redisTemplate.opsForList().rightPopAndLeftPush(sourceKey, destinationKey)
redisTemplate.opsForList().rightPopAndLeftPush(sourceKey, destinationKey, timeout, unit)
删除集合中值等于value的元素(index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素; index<0, 从尾部开始删除第一个值等于value的元素)
redisTemplate.opsForList().remove(key, index, value)
将List列表进行剪裁
redisTemplate.opsForList().trim(key, start, end)
获取当前key的List列表长度
redisTemplate.opsForList().size(key)
Set类型
添加元素
redisTemplate.opsForSet().add(key, values)
移除元素(单个值、多个值)
redisTemplate.opsForSet().remove(key, values)
删除并且返回一个随机的元素
redisTemplate.opsForSet().pop(key)
获取集合的大小
redisTemplate.opsForSet().size(key)
判断集合是否包含value
redisTemplate.opsForSet().isMember(key, value)
获取两个集合的交集(key对应的无序集合与otherKey对应的无序集合求交集)
redisTemplate.opsForSet().intersect(key, otherKey)
获取多个集合的交集(Collection var2)
redisTemplate.opsForSet().intersect(key, otherKeys)
key集合与otherKey集合的交集存储到destKey集合中(其中otherKey可以为单个值或者集合)
redisTemplate.opsForSet().intersectAndStore(key, otherKey, destKey)
key集合与多个集合的交集存储到destKey无序集合中
redisTemplate.opsForSet().intersectAndStore(key, otherKeys, destKey)
获取两个或者多个集合的并集(otherKeys可以为单个值或者是集合)
redisTemplate.opsForSet().union(key, otherKeys)
key集合与otherKey集合的并集存储到destKey中(otherKeys可以为单个值或者是集合)
redisTemplate.opsForSet().unionAndStore(key, otherKey, destKey)
获取两个或者多个集合的差集(otherKeys可以为单个值或者是集合)
redisTemplate.opsForSet().difference(key, otherKeys)
差集存储到destKey中(otherKeys可以为单个值或者集合)
redisTemplate.opsForSet().differenceAndStore(key, otherKey, destKey)
随机获取集合中的一个元素
redisTemplate.opsForSet().randomMember(key)
获取集合中的所有元素
redisTemplate.opsForSet().members(key)
随机获取集合中count个元素
redisTemplate.opsForSet().randomMembers(key, count)
获取多个key无序集合中的元素(去重),count表示个数
redisTemplate.opsForSet().distinctRandomMembers(key, count)
遍历set类似于Interator(ScanOptions.NONE为显示所有的)
redisTemplate.opsForSet().scan(key, options)
Hash类型
Long delete(H key, Object... hashKeys);
删除给定的哈希hashKeys
System.out.println(template.opsForHash().delete("redisHash","name"));
System.out.println(template.opsForHash().entries("redisHash"));
1
{class=6, age=28.1}
Boolean hasKey(H key, Object hashKey);
确定哈希hashKey是否存在
System.out.println(template.opsForHash().hasKey("redisHash","666"));
System.out.println(template.opsForHash().hasKey("redisHash","777"));
true
false
HV get(H key, Object hashKey);
从键中的哈希获取给定hashKey的值
System.out.println(template.opsForHash().get("redisHash","age"));
26
Set<HK> keys(H key);
获取key所对应的散列表的key
System.out.println(template.opsForHash().keys("redisHash"));
//redisHash所对应的散列表为{class=1, name=666, age=27}
[name, class, age]
Long size(H key);
获取key所对应的散列表的大小个数
System.out.println(template.opsForHash().size("redisHash"));
//redisHash所对应的散列表为{class=1, name=666, age=27}
3
void putAll(H key, Map<? extends HK, ? extends HV> m);
使用m中提供的多个散列字段设置到key对应的散列表中
Map<String,Object> testMap = new HashMap();
testMap.put("name","666");
testMap.put("age",27);
testMap.put("class","1");
template.opsForHash().putAll("redisHash1",testMap);
System.out.println(template.opsForHash().entries("redisHash1"));
{class=1, name=jack, age=27}
void put(H key, HK hashKey, HV value);
设置散列hashKey的值
template.opsForHash().put("redisHash","name","666");
template.opsForHash().put("redisHash","age",26);
template.opsForHash().put("redisHash","class","6");
System.out.println(template.opsForHash().entries("redisHash"));
{age=26, class=6, name=666}
List<HV> values(H key);
获取整个哈希存储的值根据密钥
System.out.println(template.opsForHash().values("redisHash"));
[tom, 26, 6]
Map<HK, HV> entries(H key);
获取整个哈希存储根据密钥
System.out.println(template.opsForHash().entries("redisHash"));
{age=26, class=6, name=tom}
Cursor<Map.Entry<HK, HV>> scan(H key, ScanOptions options);
使用Cursor在key的hash中迭代,相当于迭代器。
Cursor<Map.Entry<Object, Object>> curosr = template.opsForHash().scan("redisHash",
ScanOptions.ScanOptions.NONE);
while(curosr.hasNext()){
Map.Entry<Object, Object> entry = curosr.next();
System.out.println(entry.getKey()+":"+entry.getValue());
}
age:27
class:6
name:666
命令 | 操作 | 返回值 |
hash.delete(H key, Object... hashKeys) | 删除,可以传入多个map的key【hdel】 | Long |
hash.hasKey(key, hashKey) | 查看hash中的某个hashKey是否存在【hexists】 | Boolean |
hash.get(key, hashKey) | 获取值【hget】 | Object(HV 泛型约束对象) |
hash.multiGet(H key, Collection<HK> hashKeys) | 批量获取集合中的key对应的值【hmget】 | List<HV> |
hash.increment(H key, HK hashKey, long delta) | 对值进行+(delta值)操作【】 | Long |
hash.increment(H key, HK hashKey, double delta) | ~ | double |
hash.keys(key) | 返回map内hashKey的集合【hkeys】 | Set<HK> |
hash.lengthOfValue(H key, HK hashKey) | 返回查询键关联的值的长度,为null则返回0【hstrlen】 | Long |
hash.size(H key) | 获取hashKey的个数【hlen】 | Long |
hash.putAll(H key, Map<? extends HK, ? extends HV> m) | 相当于map的putAll【hmset】 | void |
hash.put(H key, HK hashKey, HV value) | 设置值,添加hashKey-value,hashKay相当于map的key 【hset】 | void |
hash.putIfAbsent(H key, HK hashKey, HV value) | 仅当hashKey不存在时设置值 | Boolean |
hash.values(key) | 返回value的集合【hvals】 | List<HV> |
hase.entries(H key) | 获取map【hgetall】 | Map<HK, HV> |
hash.scan(H key, ScanOptions options) | 基于游标的迭代查询【hscan】 | Cursor<Map.Entry<HK, HV>>(返回的Cursor要手动关闭,见下面示例2) |
hash.getOperations() | 返回RedisOperation,它就是redis操作的接口 | RedisOperations<H, ?> |
ZSet类型
Boolean add(K key, V value, double score);
新增一个有序集合,存在的话为false,不存在的话为true
System.out.println(template.opsForZSet().add("zset1","zset-1",1.0));
true
Long add(K key, Set<TypedTuple<V>> tuples);
新增一个有序集合
ZSetOperations.TypedTuple<Object> objectTypedTuple1 = new DefaultTypedTuple<>("zset-5",9.6);
ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>("zset-6",9.9);
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<ZSetOperations.TypedTuple<Object>>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
System.out.println(template.opsForZSet().add("zset1",tuples));
System.out.println(template.opsForZSet().range("zset1",0,-1));
[zset-1, zset-2, zset-3, zset-4, zset-5, zset-6]
Long remove(K key, Object... values);
从有序集合中移除一个或者多个元素
System.out.println(template.opsForZSet().range("zset1",0,-1));
System.out.println(template.opsForZSet().remove("zset1","zset-6"));
System.out.println(template.opsForZSet().range("zset1",0,-1));
[zset-1, zset-2, zset-3, zset-4, zset-5, zset-6]
1
[zset-1, zset-2, zset-3, zset-4, zset-5]
Long rank(K key, Object o);
返回有序集中指定成员的排名,其中有序集成员按分数值递增(从小到大)顺序排列
System.out.println(template.opsForZSet().range("zset1",0,-1));
System.out.println(template.opsForZSet().rank("zset1","zset-2"));
[zset-2, zset-1, zset-3, zset-4, zset-5]
0 //表明排名第一
Set<V> range(K key, long start, long end);
通过索引区间返回有序集合成指定区间内的成员,其中有序集成员按分数值递增(从小到大)顺序排列
System.out.println(template.opsForZSet().range("zset1",0,-1));
[zset-2, zset-1, zset-3, zset-4, zset-5]
Long count(K key, double min, double max);
通过分数返回有序集合指定区间内的成员个数
System.out.println(template.opsForZSet().rangeByScore("zset1",0,5));
System.out.println(template.opsForZSet().count("zset1",0,5));
[zset-2, zset-1, zset-3]
3
Long size(K key);
获取有序集合的成员数,内部调用的就是zCard方法
System.out.println(template.opsForZSet().size("zset1"));
6
Double score(K key, Object o);
获取指定成员的score值
System.out.println(template.opsForZSet().score("zset1","zset-1"));
2.2
Long removeRange(K key, long start, long end);
移除指定索引位置的成员,其中有序集成员按分数值递增(从小到大)顺序排列
System.out.println(template.opsForZSet().range("zset2",0,-1));
System.out.println(template.opsForZSet().removeRange("zset2",1,2));
System.out.println(template.opsForZSet().range("zset2",0,-1));
[zset-1, zset-2, zset-3, zset-4]
2
[zset-1, zset-4]
Cursor<TypedTuple<V>> scan(K key, ScanOptions options);
遍历zset
Cursor<ZSetOperations.TypedTuple<Object>> cursor = template.opsForZSet().scan("zzset1", ScanOptions.NONE);
while (cursor.hasNext()){
ZSetOperations.TypedTuple<Object> item = cursor.next();
System.out.println(item.getValue() + ":" + item.getScore());
}
zset-1:1.0
zset-2:2.0
zset-3:3.0
zset-4:6.0
集成 Spring Cache
spring缓存注解
从3.1开始,Spring引入了对Cache的支持。其使用方法和原理都类似于Spring对事务管理的支持。Spring Cache是作用在方法上的,其核心思想是这样的:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。所以在使用Spring Cache的时候我们要保证我们缓存的方法对于相同的方法参数要有相同的返回结果。
使用Spring Cache需要我们做两方面的事:
1. 声明某些方法使用缓存
2. 配置Spring对Cache的支持
和Spring对事务管理的支持一样,Spring对Cache的支持也有基于注解和基于XML配置两种方式。下面我们先来看看基于注解的方式。(基于配置的自己私下去了解即可)
基于注解的支持
@Cacheable
@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。
@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@Cacheable可以指定三个属性,value、key和condition。
参数 | 解释 | example |
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
value属性指定Cache名称
value其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以是多个Cache,当需要指定多个Cache时其是一个数组。
@Cacheable("cache1")//Cache是发生在cache1上的
public User find(Integer id) {
returnnull;
}
@Cacheable({"cache1", "cache2"})//Cache是发生在cache1和cache2上的
public User find(Integer id) {
returnnull;
}
使用key属性自定义key
key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。我们这里先来看看自定义策略
自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。
@Cacheable(value="users", key="#id")
public User find(Integer id) {
return null;
}
@Cacheable(value="users", key="#p0")
public User find(Integer id) {
returnnull;
}
@Cacheable(value="users", key="#user.id")
public User find(User user) {
returnnull;
}
@Cacheable(value="users", key="#p0.id")
public User find(User user) {
returnnull;
}
除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。
属性名称 | 描述 | 示例 |
methodName | 当前方法名 | #root.methodName |
method | 当前方法 | #root.method.name |
target | 当前被调用的对象 | #root.target |
targetClass | 当前被调用的对象的class | #root.targetClass |
args | 当前方法参数组成的数组 | #root.args[0] |
caches | 当前被调用的方法使用的Cache | #root.caches[0].name |
当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
returnnull;
}
condition属性指定发生的条件
有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。如下示例表示只有当user的id为偶数时才会进行缓存。
@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
System.out.println("find user by user " + user);
return user;
}
@CachePut
在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
一般使用在保存,更新方法中。
@CachePut也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟 @Cacheable是一样的。
@Cacheable(cacheNames = "user", key = "#id")//每次都会执行方法,并将结果存入指定的缓存中
public User find(Integer id) {
return null;
}
@CacheEvict
@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。
1、 allEntries属性
allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。
@CacheEvict(value="users",key = "#id", allEntries=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
2、 beforeInvocation属性
清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@CacheEvict(value="users",key = "#id", beforeInvocation=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
springboot基于注解的redis缓存
1.开启注解缓存
2.修改service
package com.example.demo.dept.service.impl;
import com.example.demo.dept.entity.Dept;
import com.example.demo.dept.dao.DeptMapper;
import com.example.demo.dept.service.IDeptService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author yuyongli
* @since 2021-03-04
*/
@Service
public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements IDeptService {
@Autowired
private RedisTemplate redisTemplate;
@Override
//@Cacheable(value = "depts",key = "#root.methodName")
@CachePut(value = "depts",key = "#root.targetClass")
public List<Dept> queryAll() {
List<Dept> users = this.list();
//将数据存到redis里面
return users;
//return
}
@Override
@CachePut(value = "dept",key = "#dept.deptno")
public boolean addDept(Dept dept) {
boolean save = this.save(dept);
return save;
}
@Override
@CacheEvict(value = "depts",key="#root.targetClass")
public boolean deleteDept(int deptno) {
boolean b = this.removeById(deptno);
return b;
}
}
3.测试
package com.example.demo;
import com.example.demo.dept.entity.Dept;
import com.example.demo.dept.service.IDeptService;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.ClusterOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@SpringBootTest
@RunWith(SpringRunner.class)
class DemoApplicationTests {
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Autowired
private IDeptService deptService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
public void testredis(){
redisTemplate.opsForValue().set("myKey","myValue");
System.out.println(redisTemplate.opsForValue().get("myKey"));
//list类型
redisTemplate.opsForList().leftPush("testlist","yuyongli","yuyongli");
//取值
List<String> testlist = redisTemplate.opsForList().range("testlist", 0, -1);
System.out.println(testlist);
//set类型
redisTemplate.opsForSet().add("setkey","yuyongli","30","30");
Set<String> members = redisTemplate.opsForSet().members("setkey");
System.out.println(members);
}
@Test
public void testjiqun(){
redisTemplate.opsForValue().set("test","yyl");
String test = redisTemplate.opsForValue().get("test");
System.out.println(test);
}
@Test
public void testQueryDept(){
List<Dept> depts = deptService.queryAll();
System.out.println(depts);
}
/**
* 测试添加
*/
@Test
public void addDept(){
Dept dept=new Dept();
dept.setDname("于永利测试1");
dept.setLoc("郑州");
deptService.addDept(dept);
}
@Test
public void delDept(){
deptService.deleteDept(12);
}
}
四.秒杀活动
1.简单版本
2.事务版本
乐观锁
库存中有10件商品 商品的信息自定义 同时有100个人去抢购
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
/**
* @author yuyongli
* @Date 2021/5/17
*/
@RestController
public class SecKillController
{
@GetMapping("seckill")
public void secKill(){
//
Jedis jedis=new Jedis("192.168.159.34");
//
jedis.auth("yyl");
//数据库的索引是15
jedis.select(15);
//获取商品的库存
String kcKey="pro:1";
//监视库存
jedis.watch(kcKey);
String uid="yyl"+Math.random()*100;
//获取的秒杀商品的数量
String s = jedis.get(kcKey);
if(s==null){
System.out.println("秒杀活动还没有开始!!!");
return;
}
//判断商品的数量 >0 秒杀
//<=0 秒杀活动已经结束
int num = Integer.parseInt(s);
if(num<=0){
System.out.println("秒杀活动已经结束!!!!!");
return;
}
if(num>0){
//可以秒杀 判断用户是否重复
Boolean userkey = jedis.sismember("userkey", uid);
if(userkey){
System.out.println("已经秒杀过了!!!!");
return;
}else{
Transaction multi = jedis.multi();
//秒杀成功
//1.库存-1
multi.decr(kcKey);
//2.用户信息记录 用户id set
multi.sadd("userkey",uid);
List<Object> exec = multi.exec();
//[1]
if(exec==null||exec.size()==0){
System.out.println("秒杀失败!!!!!");
}else{
System.out.println("秒杀成功");
System.out.println("秒杀成功,用户的id是"+uid);
}
}
}
}
}
分布式锁:
RedisLock
package com.example.demo.config;
/**
* @author yuyongli
* @Date 2021/5/18
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Set;
public class RedisLock {
private static Logger logger = LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate<String,Object> redisTemplate;
private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
/**
* Lock key path.
*/
private String lockKey;
/**
* 锁超时时间,防止线程在入锁以后,无限的执行等待
*/
private int expireMsecs = 60 * 1000;
/**
* 锁等待时间,防止线程饥饿
*/
private int timeoutMsecs = 10 * 1000;
private volatile boolean locked = false;
public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey + "_lock";
}
/**
* Detailed constructor with default lock expiration of 60000 msecs.
*
*/
public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs) {
this(redisTemplate, lockKey);
this.timeoutMsecs = timeoutMsecs;
}
/**
* Detailed constructor.
*
*/
public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
this(redisTemplate, lockKey, timeoutMsecs);
this.expireMsecs = expireMsecs;
}
/**
* @return lock key
*/
public String getLockKey() {
return lockKey;
}
public String get(final String key) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] data = connection.get(serializer.serialize(key));
connection.close();
if (data == null) {
return null;
}
return serializer.deserialize(data);
}
});
} catch (Exception e) {
logger.error("get redis error, key : {}", key);
}
return obj != null ? obj.toString() : null;
}
public String set(final String key,final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
connection.set(serializer.serialize(key), serializer.serialize(value));
return serializer;
}
});
} catch (Exception e) {
logger.error("get redis error, key : {}", key);
}
return obj != null ? obj.toString() : null;
}
public boolean setNX(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
connection.close();
return success;
}
});
} catch (Exception e) {
logger.error("setNX redis error, key : {}", key);
}
return obj != null ? (Boolean) obj : false;
}
private String getSet(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
connection.close();
return serializer.deserialize(ret);
}
});
} catch (Exception e) {
logger.error("setNX redis error, key : {}", key);
}
return obj != null ? (String) obj : null;
}
/**
* 获得 lock. 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
reids缓存的key是锁的key,所有的共享,
* value是锁的到期时间(注意:这里把过期时间放在value了,
没有时间上设置其超时时间) 执行过程:
* 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,
成功获得锁
* 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
*
* @return true if lock is acquired, false acquire timeouted
* @throws InterruptedException
* in case of thread interruption
*/
public synchronized boolean lock() throws InterruptedException {
int timeout = timeoutMsecs;
while (timeout >= 0) {
long expires = System.currentTimeMillis() + expireMsecs + 1;
String expiresStr = String.valueOf(expires); // 锁到期时间
if (this.setNX(lockKey, expiresStr)) {
// lock acquired
locked = true;
return true;
}
String currentValueStr = this.get(lockKey); // redis里的时间
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
// lock is expired
String oldValueStr = this.getSet(lockKey, expiresStr);
// 获取上一个锁到期时间,并设置现在的锁到期时间,
// 只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
// [分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
// lock acquired
locked = true;
return true;
}
}
timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
/*
* 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
* 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
* 使用随机的等待时间可以一定程度上保证公平性
*/
Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
}
return false;
}
/**
* Acqurired lock release.
*/
public synchronized void unlock() {
if (locked) {
redisTemplate.delete(lockKey);
locked = false;
}
}
}
controller:
@GetMapping("seckill2")
public void secKill(){
String kckey="pro:1";
String uid="yyl"+Math.random()*100;
RedisLock lock = new RedisLock(redisTemplate, kckey, 10000, 20000);
try {
if (lock.lock()) {
// 需要加锁的代码
String pronum = lock.get(kckey);
if (StringUtils.isNotEmpty(pronum)) {
//修改库存
if (Integer.parseInt(pronum) - 1 >= 0) {
lock.set(kckey, String.valueOf(Integer.parseInt(pronum) - 1));
redisTemplate.opsForSet().add("userkey",uid);
System.out.println("库存数量:" + pronum + " 成功!!!用户的id是:" + uid);
} else {
System.out.println("秒杀已经结束了呀!!!!!");
}
//return true;
}else{
System.out.println("秒杀还没有开始!!!!!");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,
// 操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做
lock.unlock();
}
// return false;
}