安装
-
进入目标目录下,wget http://download.redis.io/releases/redis-6.0.8.tar.gz
-
安装gcc套装
yum install cpp
yum install binutils
yum install glibc
yum install glibc-kernheaders
yum install glibc-common
yum install glibc-devel
yum install gcc
yum install make -
升级gcc
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
scl enable devtoolset-9 bash
-
make test
-
make install /usr/local/redis6
-
cp redis.conf /usr/local/redis6
-
cd /usr/local/redis6
mkdir log
-
修改配置文件redis.conf: daemonize yes //后台启动
bind 127.0.0.1 改为 bind 0.0.0.0 //127.0.0.1只能本机访问
0.0.0.0在服务器的环境中,指的就是服务器上所有的ipv4地址
-
启动redis服务 ./redis-server …/redis.conf
-
启动redis客户端 ./redis-cli
-
ping - pong
-
暴漏端口
firewall-cmd --list-all
设置开放的端口号
firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-port=6379/tcp --permanent
ps: –permanent表示永久生效,不加这个参数的话只会针对本次执行完命令生效,重启后就不管用了
重启防火墙
firewall-cmd --reload
Redis数据类型及使用
查命令地址
http://www.redis.cn/commands.html
八大类型
String(字符类型)
Hash(散列类型)
List(列表类型)
Set(集合类型)
SortedSet(有序集合类型,简称zset)
Bitmap(位图)
HyperLogLog(统计)
GEO(地理)
备注
命令不区分大小写,key区分大小写
内置查命令:help @类型名 eg:help @string
String
赋值
set key value
get key
同时设置/获取多个键值
MSET key value [key value …]
MGET key [key …]
数值增减
递增数字 INCR key
增加指定的整数 INCRBY key increment
递减数值 DECR key
减少指定的整数 DECRBY key decrement
使用场景
商品编号,订单号采用INCR命令生成
记录点赞数
Hash
redis hash ===> java Map<String,map<k,v>>
数据结构(java):Map<key,map<field,value>>
一次设置一个字段值 HSET key field value
一次获取一个字段值 HGET key field
一次设置多个字段值 HMSET key field value [field value …]
一次获取多个字段值 HMGET key field [field …]
获取所有字段值 hgetall key
获取某个key内的全部数量 hlen key
删除一个key/field hdel key field [field …]
使用场景
购物车
新增商品1> hset user:01 food1 1
(integer) 1
新增商品2> hset user:01 food2 1
(integer) 1
增加商品2> hincrby user:01 food2 1
(integer) 2
商品数> hlen user:01
(integer) 2
全选商品> hgetall user:01
List
向列表左边添加元素 LPUSH key value [value …]
向列表右边添加元素 RPUSH key value [value …]
查看列表 LRANGE key start stop
获取列表中元素的个数 LLEN key
使用场景
公众号推送消息:lpush wzlike:1001 news sports games
查看消息分页:lrange wzlike:1001 0 10
Set
添加元素 SADD key member [member …]
删除元素 SREM key key member [member …]
获取集合中的所有元素 SMEMBERS key
判断元素是否在集合中 SISMEMBER key member
获取集合中的元素个数 SCARD key
从集合中随机弹出一个元素,元素不删除 SRANDMEMBER key [数字]
从集合中随机弹出一个元素,出一个删一个 SPOP key [数字]
集合运算
差集:SDIFF key [key …]
交集:SINTER key [key …]
并集:SUNION key [key …]
使用场景
抽奖 SPOP SRANDMEMBER
参与抽奖总人数 SCARD
朋友圈点赞
新增点赞 sadd pub:msgid 点赞用户id
取消点赞 srem pub:msgid 点赞用户id
展示所有点赞用户 smembers pub:msgid
点赞用户数统计,就是常见的点赞红色数字 scard pub:msgid
微博共同关注:取交集
qq可能认识的人:sinter-我们都认识的人;sdiff可能认识的人
ZSet
[withscores] 连带着分数一起显示
添加元素 ZADD key score member [score member …]
按照元素分数从小到大的顺序返回索引start到stop之间的所有元素 ZRANGE key start stop [WITHSCORES]
获取元素的分数 zscore key member
删除元素 zrem key member [member …]
获取指定分数范围的元素 zrangebyscore key min max [withscores] [limit offset count]
增加某个元素的分数 zincrby key increment member
获取集合中元素的数量 zcard key
获得指定分数范围内的元素个数 zcount key min max
使用场景
1.商品销量排行榜(sorted set集合),key为goods:sellsort 分数为商品销量
商品编号1001的销量是9,商品编号1002的销量是15 zadd goods:sellsort 9 1001 15 1002
有一个客户又买了2件商品1001,商品编号1001销量增加2 zincrby goods:sellsort 2 1001
求商品销量排行前10名 zrange goods:sellsort 0 10 withscores
2.热搜 zrevrange 倒序-分数高的在前面
Redis事务
Redis的事务是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成。
Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
Redis将命令集合序列化并确保处于一个事务的命令集合连续且不被打断的执行。
Redis不支持回滚的操作
不保证原子性:redis中的一个事务中如果存在命令执行失败,那么其他命令依然会被执行,没有回滚机制
watch监控,监控的值如果被修改了,事务执行不成功
整合SpringBoot
pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
</dependencies>
application.properties
server.port=1111
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.247.129
# Redis服务器端口
spring.redis.port=6379
# Redis 服务器密码
spring.redis.password=
# 连接池最大连接数(使用负数表示没有限制)默认8
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负数表示没有限制) 默认-1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.jedis.pool.min-idle=0
spring.redis.timeout=50000
自定义RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
controller测试
@RestController
public class GoodsController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buyGoods(){
String key = "goods:001";
String result = stringRedisTemplate.opsForValue().get(key);
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));
return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
} else {
System.out.println("库存不足" + "服务端口:" + serverPort);
}
return "库存不足" + "服务端口:" + serverPort;
}
@GetMapping("/add_goods/{number}")
public String buyGoods(@PathVariable Integer number){
try{
String key = "goods:001";
String result = stringRedisTemplate.opsForValue().get(key);
Integer curNumber = result == null ? 0 : Integer.valueOf(result);
stringRedisTemplate.opsForValue().set(key,String.valueOf(curNumber + number));
} catch (Exception e){
return "添加失败" + "服务端口:" + serverPort;
}
return "添加成功" + "服务端口:" + serverPort;
}
}
分布式锁
单机锁:Lock与Synchronize
private ReentrantLock lock = new ReentrantLock();
@GetMapping("/buy_goods")
public String buyGoods() {
lock.lock();
try{
String key = "goods:001";
String result = stringRedisTemplate.opsForValue().get(key);
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
Thread.sleep(10000);
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));
return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
} else {
System.out.println("库存不足" + "服务端口:" + serverPort);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return "库存不足" + "服务端口:" + serverPort;
}
分布式锁:
1.setIfAbsent
absent 缺少的,如果没有就添加
对应redis命令:SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX
seconds – 设置键key的过期时间,单位时秒PX
milliseconds – 设置键key的过期时间,单位时毫秒NX
– 只有键key不存在的时候才会设置key的值XX
– 只有键key存在的时候才会设置key的值
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
问题1:为什么设置过期时间?
如果服务器宕机,程序走不到finally块中,为了保证解锁可以加入过期时间,达到自动解锁的效果
赋值与过期时间必须在一行,保证原子性
问题2:误删其他线程的锁
误删其他线程的锁,线程1执行超出key的过期时间,线程2抢到锁,线程1在finally中误删线程2的锁
解决:
删除前进行判断,不要误删其他的锁
private static final String REDIS_LOCK = "redis_lock";
public String buyGoods() throws InterruptedException {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try{
// 设置过期时间,到时间自动解锁
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value,60L, TimeUnit.SECONDS);
if(!flag){
return "抢锁失败,服务器忙";
}
String key = "goods:001";
String result = stringRedisTemplate.opsForValue().get(key);
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
Thread.sleep(10000);
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));
return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
} else {
System.out.println("库存不足" + "服务端口:" + serverPort);
}
} finally {
// 释放redis锁
if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
stringRedisTemplate.delete(REDIS_LOCK);
}
}
return "库存不足" + "服务端口:" + serverPort;
}
问题3:finally块的判断+del删除操作不是原子性的
解决:
1.lua脚本 官网推荐
脚本如下:
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
RedisUtil
public class RedisUtils {
private static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool = new JedisPool(jedisPoolConfig,"192.168.111.147",6379);
}
public static Jedis getJedis() throws Exception {
if(null != jedisPool){
return jedisPool.getResource();
}
throw new Exception("jedispool is not ok");
}
}
改进后:
finally {
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call('get',KEYS[1]) == ARGV[1]" +
"then" +
" return redis.call('del',KEYS[1])" +
"else" +
" return 0" +
"end";
try{
// 执行脚本
Object res = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
if("1".equals(res.toString())){
System.out.println("----- del redis lock ok");
} else {
System.out.println("----- del redis lock error");
}
} finally {
if(null != jedis){
jedis.close();
}
}
}
2.redis自身事务
改进后
finally {
while (true){
// 监控key,防止其他人修改
stringRedisTemplate.watch(REDIS_LOCK);
// 判断是不是自己的锁
if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
// 开启事务
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(REDIS_LOCK);
// 删除key,并提交事务
List<Object> list = stringRedisTemplate.exec();
// 返回空说明事务执行失败,重新尝试
if(list == null){
continue;
}
}
// 解除watch锁
stringRedisTemplate.unwatch();
break;
}
}
问题4:如何确保redisLock过期时间大于业务执行时间, 自动续期
问题5:redis异步赋值造成锁丢失
redis集群master设置key,未同步完成master挂掉,slave上位,导致key在master上有,在slave上没有
解决问题4,5
官网推荐RedLock Redisson
改进后
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods2")
public String buyGoods2() throws Exception {
RLock redissonLock = redisson.getLock(REDIS_LOCK);
redissonLock.lock();
try{
String key = "goods:001";
String result = stringRedisTemplate.opsForValue().get(key);
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0){
Thread.sleep(10000);
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));
return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
} else {
System.out.println("库存不足" + "服务端口:" + serverPort);
}
} finally {
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
}
return "库存不足" + "服务端口:" + serverPort;
}
Redis内存
查看Redis占用最大内存
配置文件redis.conf 中 maxmemory
如果不设置最大内存大小或者设置最大内存大小为0,在64为操作系统下,不限制内存大小,在32位操作系统下最多使用3GB
生产环境配置多大合适
一般推荐Redis设置内存为最大物理内存的四分之三(HashMap负载因子0.75)
修改方式
1.修改配置文件redis.conf 中 maxmemory
2.命令修改 eg: conf set maxmemory 104857600
查看内存:info memory
内存满了会怎样
报错OOM:OOM command not allowed when used memory > ‘maxmemory’
Redis内存淘汰策略
过期键删除策略
定时删除
过期后立即删除,数据新鲜度高
对cpu不友好,用处理器性能换取存储空间(时间换空间)
惰性删除
用到时候再删(被访问时候再删),内存中会有不需要的数据
对memory不友好,用存储空间换处理器性能(空间换时间)
定期删除
是前两种策略的折中
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:cpu性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
举例:redis默认每隔100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次(太浪费效率)而是随机抽取进行检查。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
总结:周期性抽查存储空间(随机抽查,重点抽查)
淘汰策略
使用惰性删除和定期删除,难免还有漏网之鱼(既没有用到,也没有被抽取到的key),当存储量接近maxmemory的时候,会触发淘汰策略
noeviction:不会驱逐任何key (出厂默认)
allkeys-lru:对所有key使用LRU算法进行删除
volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
allkeys-random:对所有key随机删除
volatile-random:对所有设置了过期时间的key随机删除
volatile-ttl:删除马上要过期的key
allkeys-lfu:对所有key使用LFU算法进行删除
volatile-lfu:对所有设置了过期时间的key进行LFU算法进行删除
生产环境推荐用:allkeys-lru
LRU算法
LRU是least Recently used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。淘汰最不常用的数据
LinkedHashMap实现
public class LRUCacheDemo<K,V> extends LinkedHashMap<K,V> {
private int capacity = 3;
public LRUCacheDemo(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > capacity;
}
public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
lruCacheDemo.put(1,"a");
lruCacheDemo.put(2,"b");
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(4,"d");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.get(2);
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(5,"e");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(6,"f");
System.out.println(lruCacheDemo.keySet());
}
}
注意点
1.继承LinkedHashMap<K,V>,重写构造方法,public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) accessOrder设为true,会把最近访问的数据放到末尾,如果是false则不会,就是按顺序添加
2.重写removeEldestEntry,通过覆盖这个方法,加入一定的条件,满足条件返回true。当put进新的值方法返回true时,便移除该map中最老的键和值。
手写实现
public class LRUCacheDemo2<K,V> {
class Node<K,V> {
K key;
V value;
Node<K,V> next;
Node<K,V> prev;
public Node(){
this.next = null;
this.prev = null;
}
public Node(K key, V value){
this.key = key;
this.value = value;
this.next = null;
this.prev = null;
}
}
class DoubleLinkedList<K,V>{
Node<K,V> head;
Node<K,V> tail;
public DoubleLinkedList(){
this.head = new Node();
this.tail = new Node();
this.head.next = tail;
this.tail.prev = head;
}
public void addHead(Node node){
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
public void remove(Node node){
node.prev.next = node.next;
node.next.prev = node.prev;
node.next = null;
node.prev = null;
}
public Node getLast(){
return tail.prev;
}
}
private int cacheSize;
private Map<Integer,Node<Integer,Integer>> map;
private DoubleLinkedList<Integer,Integer> doubleLinkedList;
public LRUCacheDemo2(int cacheSize){
this.cacheSize = cacheSize;
map = new HashMap<>();
doubleLinkedList = new DoubleLinkedList<>();
}
public int get(int key){
if(!map.containsKey(key)){
return -1;
}
Node<Integer, Integer> node = map.get(key);
doubleLinkedList.remove(node);
doubleLinkedList.addHead(node);
return node.value;
}
public void put(int key,int value){
if(map.containsKey(key)){
// update
Node<Integer, Integer> node = map.get(key);
node.value = value;
map.put(key,node);
doubleLinkedList.remove(node);
doubleLinkedList.addHead(node);
} else {
// create
// 判断长度是否足够
if(map.size() >= cacheSize){
// 删除最老的元素
Node lastNode = doubleLinkedList.getLast();
map.remove(lastNode.key);
doubleLinkedList.remove(lastNode);
}
Node node = new Node(key,value);
map.put(key,node);
doubleLinkedList.addHead(node);
}
}
public static void main(String[] args) {
LRUCacheDemo2 lruCacheDemo = new LRUCacheDemo2(3);
lruCacheDemo.put(1,1);
lruCacheDemo.put(2,2);
lruCacheDemo.put(3,3);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(4,4);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.get(2);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(5,5);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(6,6);
System.out.println(lruCacheDemo.map.keySet());
}
}
总结:
使用双向链表加HashMap 实现
map用来增加查找效率,链表用来做增删操作,
当访问数据后将最新访问的数据移动到链表头,然后删除的时候从链表尾删
如图