redis的–bigkeys参数:对redis整个keyspace进行统计(数据量大时采样,调用scan命令),寻找每种数据类型较大的keys,给出数据统计
redis-cli –bigkeys -i 0.1 -h 127.0.0.1
redis的–bigkeys参数:对redis整个keyspace进行统计(数据量大时采样,调用scan命令),寻找每种数据类型较大的keys,给出数据统计
redis-cli –bigkeys -i 0.1 -h 127.0.0.1
前言:
前两天由于某几个厂商的api出问题,导致后台任务大量堆积,又因为我这边任务流系统会重试超时任务,所以导致队列中有大量的重复任务。这时候我们要临时解决两个事情,一件事情,让一些高质量的任务优先执行; 另一件事情, 要有去重。 rabbitmq不能很好的针对这类情况去重、分优先级。
这时候我又想到了我最爱的redis… 去重? list + set 就可以解决, 优先级,zset + zrange + zrem 也可以解决… 但问题这几个命令非原子,那么怎么让他们原子? 写模块 or redis lua script . 首先在 redis 4.x 写了个简单的module,但写完了发现一件颇为重要的事情,我们线上的是3.2 …. 然后又花了点时间改成redis lua的版本。项目本身的功能实现很简单,复杂的是创意 !!!
项目名: redis_unique_queue, 项目地址,https://github.com/rfyiamcool/redis_unique_queue
主要功能介绍:
使用redis lua script 封装的去重及优先级队列方法, 达到了组合命令的原子性和节省来往的io请求的目的.
去重队列:
不仅能保证FIFO, 而且去重.
优先级去重队列:
按照优先级获取任务, 并且去重.
使用方法:
# xiaorui.cc
PriorityQueue
NewPriorityQueue(priority int, unique bool, r *redis.Pool)
Push(q string, body string, pri int) (int, error)
Pop(q string) (resp string, err error)
UniqueQueue
NewUniqueQueue(r *redis.Pool) *UniqueQueue
UniquePush(q string, body string) (int, error)
UniquePop(q string) (resp string, err error)
more..
下面是优先级去重队列的例子:
package main
// xiaorui.cc
import (
"fmt"
"github.com/rfyiamcool/redis_unique_queue"
)
func main() {
fmt.Println("start")
redis_client_config := unique_queue.RedisConfType{
RedisPw: "",
RedisHost: "127.0.0.1:6379",
RedisDb: 0,
RedisMaxActive: 100,
RedisMaxIdle: 100,
RedisIdleTimeOut: 1000,
}
redis_client := unique_queue.NewRedisPool(redis_client_config)
qname := "xiaorui.cc"
body := "message from xiaorui.cc"
u := unique_queue.NewPriorityQueue(3, true, redis_client)
// 3: 3个优先级,从1-3级
// true: 开启unique set
u.Push(qname, body, 2)
// 2, 优先级
fmt.Println(u.Pop(qname))
}
单单使用 去重队列的例子:
package main
import (
"fmt"
"github.com/rfyiamcool/redis_unique_queue"
)
func main() {
fmt.Println("start")
redis_client_config := unique_queue.RedisConfType{
RedisPw: "",
RedisHost: "127.0.0.1:6379",
RedisDb: 0,
RedisMaxActive: 100,
RedisMaxIdle: 100,
RedisIdleTimeOut: 1000,
}
redis_client := unique_queue.NewRedisPool(redis_client_config)
qname := "xiaorui.cc"
u := unique_queue.NewUniqueQueue(redis_client)
for i := 0; i < 100; i++ {
u.UniquePush(qname, "body...")
}
fmt.Println(u.Length(qname))
for i := 0; i < 100; i++ {
u.UniquePop(qname)
}
fmt.Println(u.Length(qname))
fmt.Println("end")
}
需要改进地址也是很多, 比如 加入批量操作, 对于redis连接池引入方法改进等.
END.
最近生产环境的redis服务器由于key过期不及时,现在发现时key的个数已经暴增到5000多万了。然后运维同学那边就报警了,最大内存12G,已经用了9G多了,正好下面快要双11了,让我们快些解决。
redis服务器里面堆积大量的队列状态相关的key,其实这些key可以设置有效期,或者任务完成以后删除或者过期,但是由于我们使用类库的问题,这些key既没有删除也没有过期,堆积到redis里面去了,现在我们要做的就是删除这些无用key。在删除这些keys的过程中,走了不少弯路,这里说一下我最终采用的方案。
redis的del函数可以删除单个key,也可以删除多个key,del函数官方文档可以看这里。在google之后看到目前网络上很多文章的思路是使用keys匹配返回要删除的key,然后调用del函数去删除。这种方案在数据量较小时无可厚非,但如果像我这样面临的处理的数据有5千W时,keys的阻塞问题可能会给线上生产环境带来致命的问题。所以我们需要对这种方案作出一些修改。
可喜的是自从2.8.0以后redis提供scan来遍历key,而且这个过程是非阻塞,不会影响线上生产环境。最终经过修改的方案是用scan遍历要删除的key,然后调用del删除。
下面是我用python写的用来删除key的脚本。
import sys,redis
r = redis.Redis(host="127.0.0.1", port=6379,db=0)
if len(sys.argv) <= 1:
print("必须指明匹配key字符串")
exit(1)
pattern = sys.argv[1]
cursor = 0
num = 1
while 1 :
resut = r.scan(cursor, pattern, 10000)
del_keys = []
for i in resut[1]:
key = i.decode()
del_keys.append(key)
#print("del keys len :%d" % len(result))
if len(del_keys) == 0:
break
r.delete(*del_keys)
cursor = resut[0]
print("delete keys num : %dw" % (num))
num +=1
print("donen")
如何利用我这个脚本删除符合某个规则的key哪,如以king开头的key?
下面的命令即可完成上面的问题。
python3 main.py "king*"
期间我看到网上利用keys+del的lua脚本的方案,花了一段时间把scan+del改成lua脚本来删除。但是可惜的是目前redis并不支持这么做,由于scan返回的结果是不确定的,所以禁止在其后直接调用del操作。
docker的官方镜像提供了redis的镜像,为了方便自己随时随地需要的使用,就学习一下,顺便记录下来。
参考Docker官方Redis镜像: https://hub.docker.com/_/redis/
一行命令搞定:
$> docker pull redis
说明: docker的官方镜像都可以在Docker Hub上找到,这里会自动从Docker Hub 上找到对应的镜像并下载。
下载完成之后,可以使用如下命令查看镜像:
$> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 1fb7b6c8c0d0 6 days ago 107MB (1)
oraclelinux latest 6c33a25f4a29 2 weeks ago 229MB
elasticsearch latest d1ac13423d3c 5 weeks ago 580MB
hello-world latest 1815c82652c0 4 months ago 1.84kB
这个就是我们刚刚获取的镜像。
$> docker run --name my-redis -d redis
7dabbc56e3514e9ba1705d6c6ec180e603ceff7e6011f7915ef3b0f1f5b05b35
启动后可以查看redis运行的信息:
$> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7dabbc56e351 redis "docker-entrypoint..." 35 minutes ago Up 35 minutes 6379/tcp my-redis
在虚拟机中,没有ui工具可以连接redis,要测试redis是否正常服务,需要安装redis的命令行客户端工具(redis-cli)。
$> wget http://download.redis.io/releases/redis-2.8.17.tar.gz
$> tar xzf redis-2.8.17.tar.gz
$> cd redis-2.8.17
$> make
注意: 这里是下载redis源码进行编译,编译后在src目录下就会有redis-cli工具。 源码编译过程需要使用gcc,如果没有安装的话请先用yum或者apt-get安装gcc。 如:yum install gcc或apt-get install gcc。
编译完成后:
$> cd src
$> ./redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected>
说明已经编译完成,可以使用了。
由于没有参数的情况下,默认是连接127.0.0.1:6379,这里连接失败了,先退出cli:
not connected> exit
现在我们需要知道docker中的redis的IP地址:
$> docker inspect --format '' ${容器id}
容器id我们前面已经用docker ps看到了,拷贝过来替换掉${容器id}。
$> docker inspect --format '' 7dabbc56e351
172.17.0.2
连接:
$> ./redis-cli -h 172.17.0.2
172.17.0.2:6379>
不需要指定端口,使用默认端口6379即可。
正常连接上了,现在测试一下:
172.17.0.2:6379> set first:key "hello redis"
OK
172.17.0.2:6379> get first:key
"hello redis"
172.17.0.2:6379>
成功!
上面成功连接之后,说明我们已经可以正常使用redis了,但是实际上这不是我们想要的效果,因为只能在虚拟机内连接,我们希望可以在物理机上连接,这样才可以进行开发。
现在让我们先停掉虚拟机里的dcoker:
$> docker stop ${容器id}
现在重新启动docker的redis镜像,同时指定主机端口映射容器端口:
$> docker run -p 6379:6379 --name port-map-redis -d redis
现在使用redis-cli直接连接本地:
$> ./redis-cli
127.0.0.1:6379>
说明连接本地成功。
redis版本:redis-4.0.2
运行以下命令安装预环境。
[root@redis02 redis-4.0.2]# yum -y install gcc make
下载完redis源代码后,运行以下命令进行解压缩。
[root@redis02 softwares]# tar -xzf redis-4.0.2.tar.gz
运行make命令进行编译。
make命令执行完成编译后,会在src目录下生成6个可执行文件,分别是redis-server、redis-cli、redis-benchmark、redis-check-aof、redis-check-dump、redis-sentinel。
运行make install命令。
命令执行后会将make编译生成的可执行文件拷贝到/usr/local/bin目录下,如下图。
然后,运行./utils/install_server.sh配置向导来配置redis,并且可以将redis服务加到开机自启动中。【重要】
此时redis服务已经启动了。可以通过以下命令来操作redis了。
查看redis的运行状态:
[root@redis02 redis-4.0.2]# service redis_6379 status
关闭redis服务:
[root@redis02 redis-4.0.2]# service redis_6379 stop
开启redis服务:
[root@redis02 redis-4.0.2]# service redis_6379 start
最后可以通过redis内置的客户端工具来测试下:
[root@redis02 ~]# redis-cli
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> set name mcgrady
OK
127.0.0.1:6379> get name
"mcgrady"
127.0.0.1:6379>
可以看到,redis服务已经成功配置好了!
错误信息如下:
make[3]: gcc: Command not found
/bin/sh: cc: command not found
解决方案:
因为预环境没有安装,运行以下命令安装预环境。
[root@redis02 redis-4.0.2]# yum -y install gcc make
错误信息:
zmalloc.h:50:31: error: jemalloc/jemalloc.h: No such file or directory
zmalloc.h:55:2: error: #error "Newer version of jemalloc required"
解决方案:
运行以下命令。
make MALLOC=libc
错误信息:
You need tcl 8.5 or newer in order to run the Redis test
解决方案:
运行以下命令安装tcl。
[root@redis02 redis-4.0.2]# yum -y install tcl
错误信息:
It was not possible to connect to the redis server(s); to create a disconnected multiplexer, disable AbortOnConnectFail. UnableToResolvePhysicalConnection on TIME
解决方案:
1)关闭保护模式,注意默认是打开的。
2)绑定IP,注意默认只绑定了127.0.0.1。
有用命令:
telnet 192.168.1.29 6379,可以直接测试客户端是否能连上服务器,如果通的话,基本上就没有什么问题。
ps -aux | grep redis ,查看redis的进程,看redis是否正常启动。
乐观锁大多是以数据版本号来进行成功或者失败!
举个例子:
假设某个文章的点赞数为100,此时的version我们暂定没有异常为100.
当用户A对他进行点赞的时候进行操作,那此时的点赞数为100+1、version=101,提交更新时,由于版本号大于数据库记录的版本号,数据被更新,此时数据记录的version=101。
然而在特殊情况下用户B是和用户A是同时进行操作的,也就是说,他获得的version也是100、点赞数为100,提交结果是点赞数为100+1、version=101,但是此时对数据库的版本发现当前数据记录的version也为101,不满足当前提交版本号大于数据库版本号,所以此时更新操作被驳回。
执行实验代码:
WATCH test
value = GET test
value = value+1
MULTI
SET test value
EXEC
由于WATCH 的key会被监视,会校验这个key是否被更改,如果该监视的key 在EXEC执行前被修改了,那么整个事务都会被驳回。
php实现代码
$redis = new redis();
$redis->connect('127.0.0.1', 6379);
//获取当前点赞数
$test = $redis->get("test");
$count = 1; //默认每次点赞+1
$redis->watch("test");
$redis->multi();
//设置延迟,方便测试效果。
sleep(5);
//插入抢购数据
$redis->set("test",$test+$count);
$res = $redis->exec();
if($res){
$new_test = $redis->get("test");
echo "点赞成功当前点赞数为:".$new_test."<br/>";
}else{
echo "点赞失败";exit;
}
需要注意的是如果使用同一个浏览器的多个标签页同时访问同一个URL,那么浏览器认为这些不同的请求是同一个人,会对你的每个请求进行排队,不做并发处理。不管Nginx还是Apache,都是在并发处理,只不过你的浏览器自作主张,把你的请求阻塞了。
废话不多说,首先分享一个业务场景-抢购。一个典型的高并发问题,所需的最关键字段就是库存,在高并发的情况下每次都去数据库查询显然是不合适的,因此把库存信息存入Redis中,利用redis的锁机制来控制并发访问,是一个不错的解决方案。
首先是一段业务代码:
@Transactional
public void orderProductMockDiffUser(String productId){
//1.查库存
int stockNum = stock.get(productId);
if(stocknum == 0){
throw new SellException(ProductStatusEnum.STOCK_EMPTY);
//这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的
}else{
//2.下单
orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
sotckNum = stockNum-1;
try{
Thread.sleep(100);
} catch (InterruptedExcption e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
这里有一种比较简单的解决方案,就是synchronized关键字。
public synchronized void orderProductMockDiffUser(String productId)
这就是java自带的一种锁机制,简单的对函数加锁和释放锁。但问题是这个实在是太慢了,感兴趣的可以可以写个接口用apache ab压测一下。
ab -n 500 -c 100 http://localhost:8080/xxxxxxx
下面就是redis分布式锁的解决方法。首先要了解两个redis指令
SETNX 和 GETSET,可以在redis中文网上找到详细的介绍。
SETNX就是set if not exist的缩写,如果不存在就返回保存value并返回1,如果存在就返回0。
GETSET其实就是两个指令GET和SET,首先会GET到当前key的值并返回,然后在设置当前Key为要设置Value。
首先我们先新建一个RedisLock类:
@Slf4j
@Component
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/***
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return 锁住返回true
*/
public boolean lock(String key,String value){
if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){//setNX 返回boolean
return true;
}
//如果锁超时 ***
String currentValue = stringRedisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue)<System.currentTimeMillis()){
//获取上一个锁的时间
String oldvalue = stringRedisTemplate.opsForValue().getAndSet(key,value);
if(!StringUtils.isEmpty(oldvalue)&&oldvalue.equals(currentValue)){
return true;
}
}
return false;
}
/***
* 解锁
* @param key
* @param value
* @return
*/
public void unlock(String key,String value){
try {
String currentValue = stringRedisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue)&¤tValue.equals(value)){
stringRedisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
log.error("解锁异常");
}
}
}
这个项目是springboot的项目。首先要加入redis的pom依赖,该类只有两个功能,加锁和解锁,解锁比较简单,就是删除当前key的键值对。我们主要来说一说加锁这个功能。
首先,锁的value值是当前时间加上过期时间的时间戳,Long类型。首先看到用setiFAbsent方法也就是对应的SETNX,在没有线程获得锁的情况下可以直接拿到锁,并返回true也就是加锁,最后没有获得锁的线程会返回false。 最重要的是中间对于锁超时的处理,如果没有这段代码,当秒杀方法发生异常的时候,后续的线程都无法得到锁,也就陷入了一个死锁的情况。我们可以假设CurrentValue为A,并且在执行过程中抛出了异常,这时进入了两个value为B的线程来争夺这个锁,也就是走到了注释*的地方。currentValue==A,这时某一个线程执行到了getAndSet(key,value)函数(某一时刻一定只有一个线程执行这个方法,其他要等待)。这时oldvalue也就是之前的value等于A,在方法执行过后,oldvalue会被设置为当前的value也就是B。这时继续执行,由于oldValue==currentValue所以该线程获取到锁。而另一个线程获取的oldvalue是B,而currentValue是A,所以他就获取不到锁啦。多线程还是有些乱的,需要好好想一想。
接下来就是在业务代码中加锁啦:首要要@Autowired注入刚刚RedisLock类,不要忘记对这个类加一个@Component注解否则无法注入
private static final int TIMEOUT= 10*1000;
@Transactional
public void orderProductMockDiffUser(String productId){
long time = System.currentTimeMillions()+TIMEOUT;
if(!redislock.lock(productId,String.valueOf(time)){
throw new SellException(101,"换个姿势再试试")
}
//1.查库存
int stockNum = stock.get(productId);
if(stocknum == 0){
throw new SellException(ProductStatusEnum.STOCK_EMPTY);
//这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的
}else{
//2.下单
orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
sotckNum = stockNum-1;
try{
Thread.sleep(100);
} catch (InterruptedExcption e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
redisLock.unlock(productId,String.valueOf(time));
}
大功告成了!比synchronized快了不知道多少倍,再也不会被老板骂了!
传统MySQL+ Memcached架构遇到的问题
实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:
众多NoSQL百花齐放,如何选择
最近几年,业界不断涌现出很多各种各样的NoSQL产品,那么如何才能正确地使用好这些产品,最大化地发挥其长处,是我们需要深入研究和思考的问题,实际归根结底最重要的是了解这些产品的定位,并且了解到每款产品的tradeoffs,在实际应用中做到扬长避短,总体上这些NoSQL主要用于解决以下几种问题
面对这些不同类型的NoSQL产品,我们需要根据我们的业务场景选择最合适的产品。
Redis适用场景,如何正确的使用
前面已经分析过,Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就会有疑问,似乎Redis更像一个加强版的Memcached,那么何时使用Memcached,何时使用Redis呢?
如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:
Redis支持数据的备份,即master-slave模式的数据备份。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
抛开这些,可以深入到Redis内部构造去观察更加本质的区别,理解Redis的设计。
在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。Redis只会缓存所有的 key的信息,如果Redis发现内存的使用量超过了某一个阀值,将触发swap的操作,Redis根据“swappability = age*log(size_in_memory)”计 算出哪些key对应的value需要swap到磁盘。然后再将这些key对应的value持久化到磁盘中,同时在内存中清除。这种特性使得Redis可以 保持超过其机器本身内存大小的数据。当然,机器本身的内存必须要能够保持所有的key,毕竟这些数据是不会进行swap操作的。同时由于Redis将内存 中的数据swap到磁盘中的时候,提供服务的主线程和进行swap操作的子线程会共享这部分内存,所以如果更新需要swap的数据,Redis将阻塞这个 操作,直到子线程完成swap操作后才可以进行修改。
使用Redis特有内存模型前后的情况对比:
VM off: 300k keys, 4096 bytes values: 1.3G used
VM on: 300k keys, 4096 bytes values: 73M used
VM off: 1 million keys, 256 bytes values: 430.12M used
VM on: 1 million keys, 256 bytes values: 160.09M used
VM on: 1 million keys, values as large as you want, still: 160.09M used
当 从Redis中读取数据的时候,如果读取的key对应的value不在内存中,那么Redis就需要从swap文件中加载相应数据,然后再返回给请求方。 这里就存在一个I/O线程池的问题。在默认的情况下,Redis会出现阻塞,即完成所有的swap文件加载后才会相应。这种策略在客户端的数量较小,进行 批量操作的时候比较合适。但是如果将Redis应用在一个大型的网站应用程序中,这显然是无法满足大并发的情况的。所以Redis运行我们设置I/O线程 池的大小,对需要从swap文件中加载相应数据的读取请求进行并发操作,减少阻塞的时间。
如果希望在海量数据的环境中使用好Redis,我相信理解Redis的内存设计和阻塞的情况是不可缺少的。
环境及版本
源码安装方式
ENV PHPREDIS_VERSION 3.1.3
RUN curl -L -o /tmp/redis.tar.gz https://github.com/phpredis/phpredis/archive/$PHPREDIS_VERSION.tar.gz
&& tar xfz /tmp/redis.tar.gz
&& rm -r /tmp/redis.tar.gz
&& mkdir -p /usr/src/php/ext
&& mv phpredis-$PHPREDIS_VERSION /usr/src/php/ext/redis
&& docker-php-ext-install redis
&& rm -rf /usr/src/php #如果这段不加构建的镜像将大100M
PECL安装方式
#添加扩展 redis pecl方式
RUN apk add --no-cache --update libmemcached-libs zlib
RUN set -xe
&& apk add --no-cache --update --virtual .phpize-deps $PHPIZE_DEPS
&& pecl install -o -f redis
&& echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini
&& rm -rf /usr/share/php
&& rm -rf /tmp/*
&& apk del .phpize-deps
说明:redis的缩写是REmote DIctionary Server。它是最流行的开源,高级key-value存储系统。这里说下CentOS 7上安装redis服务器方法。
项目地址:http://redis.io/
这里用的CentOS x86_64操作系统架构,所以我将仅使用适用于x86_64的epel repo软件包。请根据您的操作系统架构(EPEL URL)搜索epel repo软件包
yum install wget
wget -r --no-parent -A 'epel-release-*.rpm' http://dl.fedoraproject.org/pub/epel/7/x86_64/e/
rpm -Uvh dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-*.rpm
之后将会在/etc/yum.repos.d中创建两个epel的repo文件。分别是epel.repo和epel-testing.repo。
yum install redis
两个重要的redis服务器配置文件的路径/etc/redis.conf和/etc/redis-sentinel.conf。
systemctl start redis.service
systemctl status redis.service
redis-cli ping
如果返回结果PONG,则安装成功。
systemctl start redis.service #启动redis服务器
systemctl stop redis.service #停止redis服务器
systemctl restart redis.service #重新启动redis服务器
systemctl status redis.service #获取redis服务器的运行状态
systemctl enable redis.service #开机启动redis服务器
systemctl disable redis.service #开机禁用redis服务器
Redis Server默认侦听端口号6379,可使用SS命令查看。
ss -nlp|grep redis
学习Redis请看:http://redis.io/documentation