mariadb(mysql) redis

mariadb(mysql)

安装

  1. winodows 略
  2. linux
  • 用yum下载安装,先添加yum源,阿里的yum源mariadb版本比较老,要新版本的还是要用官方的源,具体的官方yum源最好去官网查看,有时候会出现证书失效的情况
    重新设置好yun源,可以yum clean all && yun makecache进行加载使用
1. 首先在 RHEL/CentOS 和 Fedora 操作系统中添加 MariaDB 的 YUM 配置文件 MariaDB.repo 文件。
#编辑创建mariadb.repo仓库文件
vi /etc/yum.repos.d/MariaDB.repo

2、添加repo仓库配置
# MariaDB 10.3 CentOS repository list - created 2022-04-01 08:17 UTC
# https://mariadb.org/download/
[mariadb]
name = MariaDB
baseurl = https://mirrors.aliyun.com/mariadb/yum/10.3/centos7-amd64
gpgkey=https://mirrors.aliyun.com/mariadb/yum/RPM-GPG-KEY-MariaDB
gpgcheck=1
  • 安装 yum install MariaDB-server MariaDB-client -y 注意如果是用阿里源,安装的名字不一样 是mariadb
  • 相关的命令参数
systemctl start mariadb  #启动MariaDB

systemctl stop mariadb  #停止MariaDB

systemctl restart mariadb  #重启MariaDB

systemctl enable mariadb  #设置开机启动
  • 刚安装好的mariadb默认有些设置不安全,比如有自带的匿名账户,有测试用的test表 ,root默认没密码,
    在启动mariadb后可以用mysql_secure_installation 进行初始化设置,删除匿名用户和test表
  • 基本命令和授权命令

grant语法 grant 增删改查 on 表1,表2 to 账号1@访问ip identified by 账号1密码 , 赋权完毕后记得刷新权限 flush privileges

grant select,insert,update,delete on . to "test1"@"%" identified by "12345678";

修改mysql密码 set password = PASSWORD('redhat123');
为了数据库的安全以及和其他用户协同管理数据库,就需要创建其他数据库账户,然后分配权限. `create user yuchao@'127.0.0.1' identified by 'redhat123';`
切换普通用户yuchao,查看数据库信息,发现无法看到完整的数据库列表,此事我们需要给yuchao账号进行授权
mysql使用grant命令对账户进行授权,grant命令常见格式如下
grant 权限 on 数据库.表名 to 账户@主机名            对特定数据库中的特定表授权
grant 权限 on 数据库.* to 账户@主机名              对特定数据库中的所有表给与授权
grant 权限1,权限2,权限3 on *.* to 账户@主机名      对所有库中的所有表给与多个授权
grant all privileges on *.* to 账户@主机名      对所有库和所有表授权所有权限

grant all privileges on *.* to yuchao@127.0.0.1;    这一步要用root账号去配置权限
revoke all privileges on *.* from yuchao@127.0.0.1;  移除权限


远程连接设置哦设置所有库,所有表的所有权限,赋值权限给所有ip地址的root用户

备份

  • mysqldump命令用于备份数据库数据
    导出所有数据库的所有数据 mysqldump -u root -p --all-databases > /tmp/db.dump --all-databases 也可以用-A
    导出db1、db2两个数据库的所有数据 mysqldump -uroot -proot --databases db1 db2 >/tmp/user.sql
    刚才重定向备份的数据库文件导入到mysql中 mysql -uroot -p < /tmp/db.dump
    也可以进入数据库后进入响应的表中,用source E:/pro_sql/test.sql 导入,必须先进入对应的表中再用source导入该表的数据.

中文编码配置

编辑mysql配置文件/etc/my.cnf,下入以下内容,要重启systemctl stop mariadb.service进行加载生效

[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci
log-error=/var/log/mysqld.log
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8

自定义mysql的data文件目录踩坑记录

背景:用yum安装的mariadb,在配置文件[mysqld]中新增datadir=/opt/mysql/data 自定义数据库的路径,结果启动失败.
踩坑点1:opt下的mysql以及子目录必须要mysql的用户和组权限, chown -R mysql:mysql /opt/mysql
踩坑点2:用的是阿里云的ECS镜像,里面开启了selinux安全策略,
SELinux则是基于MAC(强制访问机制),简单的说,就是程序和访问对象上都有一个安全标签(即selinux上下文)进行区分,只有对应的标签才能允许访问。否则即使权限是777,也是不能访问的。
可以用sestatus查看selinux的开启状态,那么解决办法有2种,
第一关闭selinux,关闭方法百度,但是不安全
第二种是用chcon -R --reference 原始文件夹 目标文件夹 把默认data文件的安全标签复制给自定义的data文件 ,默认是数据库文件在/var/lib/mysql中,查看文件的安全标签内容用ls -Z命令

[root@localhost opt]# ls -Z
drwxr-xr-x. mysql mysql unconfined_u:object_r:mysqld_db_t:s0 mysql

其中 unconfined_u:object_r:mysqld_db_t 对应的chcon命令的-u unconfined_u -r object_r -t mysqld_db_t   
就是unconfined_u  _u就代表-u这个参数 
这种适用单独设置新目录内每一个文件的 SELinux 设置, 不过我用这个方法设置后,还是无法启动mariadb 用chcon -R --reference却可以,
详情参考 https://blog.csdn.net/bb807777/article/details/108282433

mysql权限一览



数据库层面(db表)的权限分析

mysql安全配置方案

  1. 限制访问mysql端口的ip

windows可以通过windows防火墙或者ipsec来限制,linux下可以通过iptables来限制。

  1. 修改mysql的端口

windows下可以修改配置文件my.ini来实现,linux可以修改配置文件my.cnf来实现。

  1. 对所有用户设置强密码并严格指定对应账号的访问ip

mysql中可在user表中指定用户的访问可访问ip

  1. root特权账号的处理

建议给root账号设置强密码,并指定只容许本地登录

  1. 日志的处理

如需要可开启查询日志,查询日志会记录登录和查询语句。

  1. mysql进程运行账号

在windows下禁止使用local system来运行mysql账户,可以考虑使用network service或者自己新建一个账号,但是必须给与mysql程序所在目录的读取权限和data目录的读取和写入权限; 在linux下,新建一个mysql账号,并在安装的时候就指定mysql以mysql账户来运行,给与程序所在目录的读取权限,data所在目录的读取和写入权限。

  1. mysql运行账号的磁盘权限

1)mysql运行账号需要给予程序所在目录的读取权限,以及data目录的读取和写入权限
2)不容许给予其他目录的写入和执行权限,特别是有网站的。
3)取消mysql运行账户对于cmd,sh等一些程序的执行权限。

  1. 网站使用的mysql账户的处理
    新建一个账户,给予账户在所使用数据库的所有权限即可。这样既能保证网站对所对应的数据库的全部操作,也能保证账户不会因为权限过高而影响安全。给予单个数据库的所有权限的账户不会拥有super, process, file等管理权限的。 当然,如果能很明确是的知道,我的网站需要哪些权限,还是不要多给权限,因为很多时候发布者并不知道网站需要哪些权限,我才建议上面的配置。而且我指的通用的,具体到只有几台机器,不多的情况下,我个人建议还是给予只需要的权限,具体可参考上面的表格的建议。

  2. 删除无用数据库
    test数据库对新建的账户默认有权限

mysql入侵提权分析及防止措施

  1. udf提权

此方式的关键导入一个dll文件,个人认为只要合理控制了进程账户对目录的写入权限即可防止被导入dll文件;然后如果万一被攻破,此时只要进程账户的权限够低,也没办执行高危操作,如添加账户等。

  1. 写入启动文件

这种方式同上,还是要合理控制进程账户对目录的写入权限。

  1. 当root账户被泄露

如果没有合理管理root账户导致root账户被入侵,此时数据库信息肯定是没办法保证了。但是如果对进程账户的权限控制住,以及其对磁盘的权限控制,服务器还是能够保证不被沦陷的。

  1. 普通账户泄露(上述所说的,只对某个库有所有权限的账户)
    此处说的普通账户指网站使用的账户,我给的一个比较方便的建议是直接给予特定库的所有权限。账户泄露包括存在注入及web服务器被入侵后直接拿到数据库账户密码。
    此时,对应的那个数据库数据不保,但是不会威胁到其他数据库。而且这里的普通账户无file权限,所有不能导出文件到磁盘,当然此时还是会对进程的账户的权限严格控制。
    普通账户给予什么样的权限可以见上表,实在不会就直接给予一个库的所有权限。

Django配置

DATABASES = {
    # 'default': {
    #     'ENGINE': 'django.db.backends.sqlite3',
    #     'NAME': BASE_DIR / 'db.sqlite3',
    # }
    'default': {
        # 'ENGINE': 'django.db.backends.mysql',
        'ENGINE': 'dj_db_conn_pool.backends.mysql',
        'NAME': 'luffy',
        'PORT': 3306,
        'HOST': 'ip地址',
        'USER': 'databasename',
        'PASSWORD': 'pwd',
        # 'OPTIONS': {
        #     'charset': 'utf8mb4', # 连接选项配置,mysql8.0以上无需配置
        # },
        'POOL_OPTIONS': {  # 连接池的配置信息
            'POOL_SIZE': 10,  # 连接池默认创建的链接对象的数量
            'MAX_OVERFLOW': 10  # 连接池默认创建的链接对象的最大数量
        }
    }
}

主从同步

MySQL数据库的主从复制方案,是其自带的功能,并且主从复制并不是复制磁盘上的数据库文件,而是通过binlog日志复制到需要同步的从服务器上。
MySQL数据库支持单向、双向、链式级联,等不同业务场景的复制。在复制的过程中,一台服务器充当主服务器(Master),接收来自用户的内容更新,而一个或多个其他的服务器充当从服务器(slave),接收来自Master上binlog文件的日志内容,解析出SQL,重新更新到Slave,使得主从服务器数据达到一致。
在生产环境中,MySQL主从复制都是异步的复制方式,即不是严格的实时复制,但是给用户的体验都是实时的。
MySQL主从复制集群功能使得MySQL数据库支持大规模高并发读写成为可能,且有效的保护了服务器宕机的数据备份。

主从复制的逻辑有以下几种

  • 一主一从,单向主从同步模式,只能在Master端写入数据

  • 一主多从

  • 双主主复制逻辑架构,此架构可以在Master1或Master2进行数据写入,或者两端同事写入(特殊设置)

  • 配置文件配置 log-bin的文件目录和主从数据库的server-id

- 修改配置文件
vim /etc/my.cnf
#修改内容
#解释:server-id服务的唯一标识(主从之间都必须不同);log-bin启动二进制日志名称为mysql-bin 
[mysqld]
server-id=1
# log-bin 可以写路径+文件名,也可以单独写文件名,默认就是在根目录下/data/
#配置文件写路径要注意 要用/ 不能用\
log-bin="D:/mysql_56/data/mysql-test"
#同步错误记录文件
log-error="D:/mysql_56/data/mysql-error"
#主从同步时,可以主库设置要同步的库,或者要忽略同步的库,如果要忽略多个数据库,写多行即可
#可以只在主库设定同步的库,表,也可以在从库中设定,也可以主从都写,相互对应,比如主库的 binlog-do-db=mysql 对应从库的replicate-do-db=mysql
#binlog-ignore-db=mysql
#binlog-do-db=mysql
#也可以在从库中写要复制的库或者表.
#replicate-do-db    设定需要复制的数据库
#replicate-ignore-db 设定需要忽略的复制数据库
#replicate-do-table  设定需要复制的表
#replicate-ignore-table 设定需要忽略的复制表
#replicate-wild-do-table 同replication-do-table功能一样,但是可以通配符
#replicate-wild-ignore-table 同replication-ignore-table功能一样,但是可以加通配符

character-set-server=utf8
collation-server=utf8_general_ci
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8

#重启mariadb
systemctl start mariadb
  • 查看主库状态 show master status 可以看到如下页面
    MariaDB [(none)]> show master status
    -> ;
    +------------------+----------+--------------+------------------+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +------------------+----------+--------------+------------------+
    | mysql-bin.000001 | 575 | | |
    +------------------+----------+--------------+------------------+
    1 row in set (0.00 sec)

    File是二进制日志文件名,Position 是日志开始的位置。后面从库会用到!!!!!

    从库会根据设定的周期查询主库的Position的位置,如果一旦发生变化,就会同步

  • master主库添加从库账号

  1.新建用于主从同步的用户chaoge,允许登录的从库是'192.168.178.130'
  create user 'chaoge'@'192.168.178.130' identified by 'redhat';

  2.#题外话:如果提示密码太简单不复合策略加在前面加这句
  mysql> set global validate_password_policy=0;

  3.给从库账号授权,说明给chaoge从库复制的权限,在192.168.178.130机器上复制
  grant replication slave on *.* to 'chaoge'@'192.168.178.130';
  #检查主库创建的复制账号
  select user,host from mysql.user;
  #检查授权账号的权限
  show grants for chaoge@'192.168.178.130';
  也可以用select * from mysql.user where user='chaoge' and host='192.168.178.130' \G;
  #实现对主数据库锁表只读,防止数据写入,数据复制失败
  flush table with read lock;
  4. 锁表后进行数据导出备份 mysqldump -A 或者mysqldump -database 导出指定的数据库
  5. 确保数据导出后,没有数据插入,完毕再查看主库状态 show master status;
  6. 导出数据完毕后,解锁主库,恢复可写  unlock tables;
  • slave从库配置
1. 修改Slave的/etc/my.cnf,写入server-id
  [mysqld]
  server-id=3
2.重启数据库
systemctl restart mariadb
3.检查Slava从数据库的各项参数
show variables like 'log_bin';
show variables like 'server_id';
4.恢复主库Master的数据导入到Slave库
导入数据(注意sql文件的路径)
mysql>source /data/all.sql;
方法二:
#mysql -uroot -p  < abc.sql 
5.配置复制的参数,Slave从库连接Master主库的配置
mysql > change master to master_host='192.168.178.129',
master_user='chaoge',
master_password='redhat',
master_log_file='mysql-bin.000001',
master_log_pos=575;
6.启动从库的同步开关,测试主从复制的情况
start slave;
7.查看复制状态
show slave status\G;
具体查看下面2个线程状态
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
如果2个线程不都是yes,可以继续往下翻,在Last_io_error中查看错误原因,这个例子中是因为master和slave的id重复了,也可以在stop slave后用全局set global sever_id = 1 进行修改,改完再start slave启动
![](https://img2022.cnblogs.com/blog/1610779/202203/1610779-20220314223611802-469456393.png)

  • 注意此处还未配置从库的只读模式,只需在slave服务器上配置/etc/my.cnf,加上以下配置,并且在slave上创建普通用户,使用普通用户主从同步即可达到只读的效果
    如果用root用户,无法达到readonly,这是一个坑
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci
log-error=/var/log/mysqld.log
server-id=3
read-only=true #设置只读
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8

redis

  • 安装部分 编译安装下载下来的都是已经编译过的文件有makefile文件,可以考虑把所有bin文件装在根目录下的/src下,cd /src/后 运行make && make install
  • redis可执行文件
    ./redis-benchmark //用于进行redis性能测试的工具
    ./redis-check-dump //用于修复出问题的dump.rdb文件
    ./redis-cli //redis的客户端
    ./redis-server //redis的服务端
    ./redis-check-aof //用于修复出问题的AOF文件
    ./redis-sentinel //用于集群管理
    
    • 配置文件内容 完整的可以参考 https://blog.csdn.net/suprezheng/article/details/90679790
    bind 192.168.248.140  # 不少人都误解了。以为这个设置是只对客户端IP的连接限制,其实这是错误的!错误的!错误的!bind是你服务器的IP地址
    protected-mode yes	#设置保护模式 开启protected-mode保护模式,如果开启了保护模式,并且bind未配置,密码也未设置。redis只接受来自本机的连接。如果要远程访问,设置为no
    requirepass 192406	设置远程访问密码
    port 6379		#端口
    tcp-backlog 511
    timeout 0
    tcp-keepalive 300
    daemonize yes		# 守护进程方式进行,如果设置no,把终端窗口关闭,redis服务也关闭了,无法后台运行
    supervised no
    pidfile /var/run/redis_6379.pid	#自定义pid文件路径
    loglevel notice
    logfile /var/log/redis/redis.log #自定义log文件路径
    databases 16		# 默认使用的数据库是0。可以通过”SELECT 【数据库序号】“命令选择一个数据库,序号从0开始
    dir /var/lib/redis	#设置数据文件的保存路径,logfile pidfile dbfilename等都以此为根路径
    #rdb的永久保存规则 
    #官方出厂配置默认是 900秒内有1个更改,300秒内有10个更改以及60秒内有10000个更改,则将内存中的
    #数据快照写入磁盘。
    # save ""
    save 900 1
    save 300 10
    save 60 10000
    
    dbfilename dump.rdb	#设置rdb的文件名字
    #当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作
    #过info中的rdb_last_bgsave_status了解RDB持久化是否有错误
    stop-writes-on-bgsave-error yes
    rdbcompression yes
    rdbchecksum yes
    
    
    #默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写
    #操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
    #默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但是redis如果中途宕机,会导致可
    #能有几分钟的数据丢失,根据save来策略进行持久化,Append Only File是另一种持久化方式,可以提供更好的
    #持久化特性。Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这
    #个文件的数据读入内存里,先忽略RDB文件。若开启rdb则将no改为yes
    
    appendonly no
    appendfilename "appendonly.aof"
    appendfsync everysec
    
    slave-serve-stale-data yes
    slave-read-only yes
    repl-diskless-sync no
    repl-diskless-sync-delay 5
    repl-disable-tcp-nodelay no
    slave-priority 100
    no-appendfsync-on-rewrite no
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    aof-load-truncated yes
    lua-time-limit 5000
    slowlog-log-slower-than 10000
    slowlog-max-len 128
    latency-monitor-threshold 0
    notify-keyspace-events ""
    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64
    list-max-ziplist-size -2
    list-compress-depth 0
    set-max-intset-entries 512
    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64
    hll-sparse-max-bytes 3000
    activerehashing yes
    client-output-buffer-limit normal 0 0 0
    client-output-buffer-limit slave 256mb 64mb 60
    client-output-buffer-limit pubsub 32mb 8mb 60
    hz 10
    aof-rewrite-incremental-fsync yes
    
    • 启动redis服务端
    启动redis非常简单,直接./redis-server就可以启动服务端了,还可以用下面的方法指定要加载的配置文件:./redis-server ../redis.conf
    
    • 使用redis客户端
    执行客户端命令即可进入
    ./redis-cli  
    #测试是否连接上redis
    127.0.0.1:6379 > ping
    返回pong代表连接上了
    如果服务端设置了绑定ip 自定义端口 以及访问密码, 那么要用到-u -p -a[au]
    redis-cli -h 10.16.244.3 -p 6379
    

库 redis默认有16个库,0-15索引值,用select 索引值进行切换

数据类型

redis可以理解成一个全局的大字典,key就是数据的唯一标识符。根据key对应的值不同,可以划分成5个基本数据类型。

redis = {
    "name":"yuan",
    "scors":["100","89","78"],
    "info":{
        "name":"rain"
        "age":22
    },
    "s":{item1,itme2,..}
}

1. string类型:
	字符串类型,是 Redis 中最为基础的数据存储类型,它在 Redis 中是二进制安全的,也就是byte类型。
	单个数据的最大容量是512M。
		key: 值
	
2. hash类型:
	哈希类型,用于存储对象/字典,对象/字典的结构为键值对。key、域、值的类型都为string。域在同一个hash中是唯一的。
		key:{
            域(属性): 值,
            域:值,            
            域:值,
            域:值,
            ...
		}
3. list类型:
	列表类型,它的子成员类型为string。
		key: [值1,值2, 值3.....]
4. set类型:
	无序集合,它的子成员类型为string类型,元素唯一不重复,没有修改操作。
		key: {值1, 值4, 值3, ...., 值5}

5. zset类型(sortedSet):
	有序集合,它的子成员值的类型为string类型,元素唯一不重复,没有修改操作。权重值(score,分数)从小到大排列。
		key: {
			值1 权重值1(数字);
			值2 权重值2;
			值3 权重值3;
			值4 权重值4;
		}

string(字符串)

  • SET/SETEX/MSET/MSETNX
  • GET/MGET
  • GETSET
  • INCR/DECR
  • DEL

1. 设置键值

set 设置的数据没有额外操作时,是不会过期的。

set key value

设置键为name值为yuan的数据

set name yuan
set name rain # 一个变量可以设置多次

注意:redis中的所有数据操作,如果设置的键不存在则为添加,如果设置的键已经存在则修改。

设置一个键,当键不存在时才能设置成功,用于一个变量只能被设置一次的情况。

setnx  key  value

一般用于给数据加锁(分布式锁)

127.0.0.1:6379> setnx goods_1 101
(integer) 1
127.0.0.1:6379> setnx goods_1 102
(integer) 0  # 表示设置不成功

127.0.0.1:6379> del goods_1
(integer) 1
127.0.0.1:6379> setnx goods_1 102
(integer) 1

2. 设置键值的过期时间

redis中可以对一切的数据进行设置有效期。以秒为单位

setex key seconds value

设置键为goods_1值为101过期时间为10秒的数据

setex name 10 goods_1

3. 关于设置保存数据的有效期

setex 添加保存数据到redis,同时设置有效期,格式:

setex key time value

4. 设置多个键值

mset key1 value1 key2 value2 ...

例3:设置键为a1值为goland、键为a2值为java、键为a3值为c

mset a1 goland a2 java a3 c

5. 字符串拼接值

常见于大文件上传

append key value

向键为a1中拼接值haha

set title "我的"
append title "redis"
append title "学习之路"

6. 根据键获取值

根据键获取值,如果不存在此键则返回nil

get key

获取键name的值

get name

根据多个键获取多个值

mget key1 key2 ...

获取键a1、a2、a3的值

mget a1 a2 a3

getset:设置key的新值,返回旧值

redis> GETSET db mongodb    # 没有旧值,返回 nil
(nil)
redis> GET db
"mongodb"

redis> GETSET db redis      # 返回旧值 mongodb
"mongodb"

redis> GET db
"redis"

7. 自增自减

web开发中的电商抢购、秒杀。游戏里面的投票、攻击计数。系统中计算当前在线人数、

set id 1
incr id   # 相当于id+1
get id    # 2
incr id   # 相当于id+1
get id    # 3

set goods_id_1 10
decr goods_id_1  # 相当于 id-1
get goods_id_1    # "9"
decr goods_id_1   # 相当于id-1
get goods_id_1    # 8

 set age 22
 incrby age 2 # 自增自减大于1的值时候用incrby

8. 获取字符串的长度

set name xiaoming
strlen name  # 8 

9. 比特流操作(注意经典使用场景)

经典使用在签到上,可以大幅度节约空间,由于比特流特性是0和1,用来做真假判断可以使用.假设8个人的签到,比起一个个设置哈嘻记录,

可以创建8比特的数据 mykey 00000011在对应的索引位置上用1代表真,0代表假,这样8个人的签到与否数据就占用1字节,如果记录8万人的签到只占了1万字节,≈10kb多一点,还不到1mb

1字节=8比特 1kb = 1024字节 1mb = 1024kb 1gb = 1024mb

1个int8就是一个字节,一个中文:3个字节

SETBIT     # SETBIT key offset value 按从左到右的偏移量设置一个bit数据的值 
GETBIT     # 获取一个bit数据的值
BITCOUNT   # 统计字符串被设置为1的bit数.
BITPOS     # 返回字符串里面第一个被设置为1或者0的bit位。

案例1:

SETBIT mykey 7 1
# 00000001
getbit mykey 7
# 00000001
SETBIT mykey 4 1
# 00001001
SETBIT mykey 15 1
# 0000100100000001

我们知道 'a' 的ASCII码是 97。转换为二进制是:01100001。offset的学名叫做“偏移” 。二进制中的每一位就是offset值啦,比如在这里 offset 0 等于 ‘0’ ,offset 1等于 '1' ,offset 2 等于 '1',offset 6 等于 '0' ,没错,offset是从左往右计数的,也就是从高位往低位

我们通过SETBIT 命令将 andy中的 'a' 变成 'b' 应该怎么变呢?

也就是将 01100001 变成 01100010 (b的ASCII码是98),这个很简单啦,也就是将'a'中的offset 6从0变成1,将offset 7 从1变成0 。

案例2:签到系统

setbit user_1 6 1
setbit user_1 5 1
setbit user_1 4 0
setbit user_1 3 1
setbit user_1 2 0
setbit user_1 1 1
setbit user_1 0 1
BITCOUNT user_1 # 统计一周的打卡情况

key操作

redis中所有的数据都是通过key(键)来进行操作,这里我们学习一下关于任何数据类型都通用的命令。

(1)查找键

参数支持简单的正则表达式

keys pattern

查看所有键

keys *

例子:

# 查看名称中包含`a`的键
keys *a*
# 查看以a开头的键
keys a*
# 查看以a结尾的键
keys *a

(2)判断键是否存在

如果存在返回1,不存在返回0

exists key1

判断键title是否存在

exists title

(3)查看键的的值的数据类型

type key

# string    字符串
# hash      哈希类型
# list      列表类型
# set       无序集合
# zset      有序集合

查看键的值类型

type a1
# string
sadd member_list xiaoming xiaohong xiaobai
# (integer) 3
type member_list
# set
hset user_1 name xiaobai age 17 sex 1
# (integer) 3
type user_1
# hash
lpush brothers zhangfei guangyu liubei xiaohei
# (integer) 4
type brothers
# list

zadd achievements 61 xiaoming 62 xiaohong 83 xiaobai  78 xiaohei 87 xiaohui 99 xiaolong
# (integer) 6
type achievements
# zset

(4)删除键以及键对应的值

del key1 key2 ...

(5)查看键的有效期

ttl key

# 结果结果是秒作为单位的整数
# -1 表示永不过期
# -2 表示当前数据已经过期,查看一个不存在的数据的有效期就是-2

(6)设置key的有效期

给已有的数据重新设置有效期,redis中所有的数据都可以通过expire来设置它的有效期。有效期到了,数据就被删除。

expire key seconds

(7)清空key

flushall   慎用,一旦执行,则redis所有数据库0~15的全部key都会被清除
flushdb   清空当前选定的库的key 需要先用select 选定库

(8)key重命名

rename  oldkey newkey

把name重命名为username

set name yuan
rename name username
get username

select切换数据库

redis的配置文件中,默认有0~15之间的16个数据库,默认操作的就是0号数据库
select <数据库ID>

操作效果:

# 默认处于0号库
127.0.0.1:6379> select 1
OK
# 这是在1号库
127.0.0.1:6379[1]> set name xiaoming
OK
127.0.0.1:6379[1]> select 2
OK
# 这是在2号库
127.0.0.1:6379[2]> set name xiaohei
OK

list(数组)

队列,列表的子成员类型为string

lpush key value

rpush key value

linsert key after|before 指定元素 value

lindex key index

lrange key start stop

lset key index value

lrem key count value

(1)添加子成员

# 在左侧(前)添加一条或多条数据
lpush key value1 value2 ...
# 在右侧(后)添加一条或多条数据
rpush key value1 value2 ...

# 在指定元素的左边(前)/右边(后)插入一个或多个数据
linsert key before 指定元素 value1 value2 ....
linsert key after 指定元素 value1 value2 ....

从键为brother的列表左侧添加一个或多个数据liubei、guanyu、zhangfei

lpush brother liubei
# [liubei]
lpush broth	er guanyu zhangfei xiaoming
# [xiaoming,zhangfei,guanyu,liubei]

从键为brother的列表右侧添加一个或多个数据,xiaohong,xiaobai,xiaohui

rpush brother xiaohong
# [xiaoming,zhangfei,guanyu,liubei,xiaohong]
rpush brother xiaobai xiaohui
# [xiaoming,zhangfei,guanyu,liubei,xiaohong,xiaobai,xiaohui]

从key=brother的xiaohong的列表位置左侧添加一个数据,xiaoA,xiaoB

linsert brother before xiaohong xiaoA
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaohong,xiaobai,xiaohui]
linsert brother before xiaohong xiaoB
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaoB,xiaohong,xiaobai,xiaohui]

从key=brother,key=xiaohong的列表位置右侧添加一个数据,xiaoC,xiaoD

linsert brother after xiaohong xiaoC
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaohong,xiaoC,xiaobai,xiaohui]
linsert brother after xiaohong xiaoD
# [xiaoming,zhangfei,guanyu,liubei,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]

注意:当列表如果存在多个成员值一致的情况下,默认只识别第一个。

127.0.0.1:6379> linsert brother before xiaoA xiaohong
# [xiaoming,zhangfei,guanyu,liubei,xiaohong,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]
127.0.0.1:6379> linsert brother before xiaohong xiaoE
# [xiaoming,zhangfei,guanyu,liubei,xiaoE,xiaohong,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]
127.0.0.1:6379> linsert brother after xiaohong xiaoF
# [xiaoming,zhangfei,guanyu,liubei,xiaoE,xiaohong,xiaoF,xiaoA,xiaohong,xiaoD,xiaoC,xiaobai,xiaohui]

(2)基于索引获取列表成员

根据指定的索引(下标)获取成员的值,负数下标从右边-1开始,逐个递减

lindex key index

获取brother下标为2以及-2的成员

del brother
lpush brother guanyu zhangfei xiaoming
lindex brother 2
# "guanyu"
lindex brother -2
# "zhangfei"

(3)获取列表的切片

闭区间[包括stop]

lrange key start stop

操作:

del brother
rpush brother liubei guanyu zhangfei xiaoming xaiohong
# 获取btother的全部成员
lrange brother 0 -1
# 获取brother的前2个成员
lrange brother 0 1
# 获取brother的后2个成员
lrange brother -2 -1

(4)获取列表的长度

llen key

获取brother列表的成员个数

llen brother

(5)按索引设置值

lset key index value
# 注意:
# redis的列表也有索引,从左往右,从0开始,逐一递增,第1个元素下标为0
# 索引可以是负数,表示尾部开始计数,如`-1`表示最后1个元素

修改键为brother的列表中下标为4的元素值为xiaohongmao

lset brother 4 xiaohonghong

(6)删除指定成员

移除并获取列表的第一个成员或最后一个成员

lpop key  # 第一个成员出列
rpop key  # 最后一个成员出列

获取并移除brother中的第一个成员

lpop brother
# 开发中往往使用rpush和lpop实现队列的数据结构->实现入列和出列
lrem key count value

# 注意:
# count表示删除的数量,value表示要删除的成员。该命令默认表示将列表从左侧前count个value的元素移除
# count==0,表示删除列表所有值为value的成员
# count >0,表示删除列表左侧开始的前count个value成员
# count <0,表示删除列表右侧开始的前count个value成员

del brother
rpush brother A B A C A
lrem brother 0 A
["B","C"]

del brother
rpush brother A B A C A
lrem brother -2 A
["A","B","C"]

del brother
rpush brother A B A C A
lrem brother 2 A
["B","C","A"]

(7)对一个列表进行修剪

让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
语法 LTRIM KEY_NAME START STOP
如果 start 下标比列表的最大下标 end ( LLEN list 减去 1 )还要大,或者 start > stop , LTRIM 返回一个空列表(因为 LTRIM 已经将整个列表清空)。
如果 stop 下标比 end 下标还要大,Redis将 stop 的值设置为 end 。
返回值
命令执行成功时,返回 ok 。

127.0.0.1:6379> lrange datalist 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
5) "2"
6) "1"
7) "1,2,3,4,5,6"
8) "10"
127.0.0.1:6379> ltrim datalist 0 3
OK
127.0.0.1:6379> lrange datalist 0 -1
1) "6"
2) "5"
3) "4"
4) "3"

hash(哈希)

语法

1、常用命令:  
hget,hset,hgetall 等。  
2、应用场景:  
我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:  
           用户ID,为查找的key,  
           存储的value用户对象包含姓名name,年龄age,生日birthday 等信息,  
  如果用普通的key/value结构来存储,主要有以下2种存储方式:  
       第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,  
           如:set u001 "李三,18,20010101"  
           这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。  
       第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,  
           如:mset user:001:name "李三 "user:001:age18 user:001:birthday "20010101"  
           虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。  
    那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,  
    并提供了直接存取这个Map成员的接口,  
        如:hmset user:001 name "李三" age 18 birthday "20010101"     
            也就是说,Key仍然是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值,  
            这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过   
            key(用户ID) + field(属性标签) 操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。  
  
          这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。  
3、实现方式:  
   上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。

hset key field value

hget key field

hgetall info

hmget key field1 field2 ...

hincrby key field number

专门用于结构化的数据信息。对应的就是map/结构体

结构:

键key:{
   	域field: 值value,
   	域field: 值value,
   	域field: 值value,
}

(1)设置指定键的属性/域

设置指定键的单个属性

hset key field value

设置键 user_1的属性namexiaoming

127.0.0.1:6379> hset user_1 name xiaoming   # user_1没有会自动创建
(integer) 1
127.0.0.1:6379> hset user_1 name xiaohei    # user_1中重复的属性会被修改
(integer) 0
127.0.0.1:6379> hset user_1 age 16          # user_1中不存在的属性会被新增
(integer) 1
127.0.0.1:6379> hset user:1 name xiaohui    # user:1会在redis界面操作中以:作为目录分隔符
(integer) 1
127.0.0.1:6379> hset user:1 age 15
(integer) 1
127.0.0.1:6379> hset user:2 name xiaohong age 16  # 一次性添加或修改多个属性

(2)获取指定键的域/属性的值

获取指定键所有的域/属性

hkeys key

获取键user的所有域/属性

127.0.0.1:6379> hkeys user:2
1) "name"
2) "age"
127.0.0.1:6379> hkeys user:3
1) "name"
2) "age"
3) "sex"

获取指定键的单个域/属性的值

hget key field

获取键user:3属性name的值

127.0.0.1:6379> hget user:3 name
"xiaohong"

获取指定键的多个域/属性的值

hmget key field1 field2 ...

获取键user:2属性nameage的值

127.0.0.1:6379> hmget user:2 name age
1) "xiaohong"
2) "16"

获取指定键的所有值

hvals key

获取指定键的所有域值对

127.0.0.1:6379> hvals user:3
1) "xiaohong"
2) "17"
3) "1"

(3)获取hash的所有域值对

127.0.0.1:6379> hset user:1 name xiaoming age 16 sex 1
(integer) 3
127.0.0.1:6379> hgetall user:1
1) "name"
2) "xiaoming"
3) "age"
4) "16"
5) "sex"
6) "1"

(4)删除指定键的域/属性

hdel key field1 field2 ...

删除键user:3的属性sex/age/name,当键中的hash数据没有任何属性,则当前键会被redis删除

hdel user:3 sex age name

(5)判断指定属性/域是否存在于当前键对应的hash中

hexists   key  field

判断user:2中是否存在age属性

127.0.0.1:6379> hexists user:3 age
(integer) 0
127.0.0.1:6379> hexists user:2 age
(integer) 1
127.0.0.1:6379> 

(6)属性值自增自减

hincrby key field number

给user:2的age属性在原值基础上+/-10,然后在age现有值的基础上-2

# 按指定数值自增
127.0.0.1:6379> hincrby user:2 age 10
(integer) 77
127.0.0.1:6379> hincrby user:2 age 10
(integer) 87

# 按指定数值自减
127.0.0.1:6379> hincrby user:2 age -10
(integer) 77
127.0.0.1:6379> hincrby user:2 age -10

集合

无序集合,重点就是去重和无序。

(1)添加元素

sadd key member1 member2 ...

向键authors的集合中添加元素zhangsanlisiwangwu

sadd authors zhangsan lisi wangwu

(2)获取集合的所有的成员

smembers key

获取键authors的集合中所有元素

smembers authors

(3)获取集合的长度

scard keys 

获取s2集合的长度

sadd s2 a b c d e

127.0.0.1:6379> scard s2
(integer) 5

(4)随机抽取一个或多个元素

抽取出来的成员被删除掉

spop key [count=1]

# 注意:
# count为可选参数,不填则默认一个。被提取成员会从集合中被删除掉

随机获取s2集合的成员

sadd s2 a c d e

127.0.0.1:6379> spop s2 
"d"
127.0.0.1:6379> spop s2 
"c"

(5)删除指定元素

srem key value

删除键authors的集合中元素wangwu

srem authors wangwu

(6)交集、差集和并集

推荐、(协同过滤,基于用户、基于物品)

sinter  key1 key2 key3 ....    # 交集、比较多个集合中共同存在的成员
sdiff   key1 key2 key3 ....    # 差集、比较多个集合中不同的成员
sunion  key1 key2 key3 ....    # 并集、合并所有集合的成员,并去重
del user:1 user:2 user:3 user:4
sadd user:1 1 2 3 4     # user:1 = {1,2,3,4}
sadd user:2 1 3 4 5     # user:2 = {1,3,4,5}
sadd user:3 1 3 5 6     # user:3 = {1,3,5,6}
sadd user:4 2 3 4       # user:4 = {2,3,4}

# 交集
127.0.0.1:6379> sinter user:1 user:2
1) "1"
2) "3"
3) "4"
127.0.0.1:6379> sinter user:1 user:3
1) "1"
2) "3"
127.0.0.1:6379> sinter user:1 user:4
1) "2"
2) "3"
3) "4"

127.0.0.1:6379> sinter user:2 user:4
1) "3"
2) "4"

# 并集
127.0.0.1:6379> sunion user:1 user:2 user:4
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"

# 差集
127.0.0.1:6379> sdiff user:2 user:3
1) "4"  # 此时可以给user:3推荐4

127.0.0.1:6379> sdiff user:3 user:2
1) "6"  # 此时可以给user:2推荐6

127.0.0.1:6379> sdiff user:1 user:3
1) "2"
2) "4"

zset有序集合

有序集合(score/value),去重并且根据score权重值来进行排序的。score从小到大排列。

(1)添加成员

zadd key score1 member1 score2 member2 score3 member3 ....

设置榜单achievements,设置成绩和用户名作为achievements的成员

127.0.0.1:6379> zadd achievements 61 xiaoming 62 xiaohong 83 xiaobai  78 xiaohei 87 xiaohui 99 xiaolan
(integer) 6
127.0.0.1:6379> zadd achievements 85 xiaohuang 
(integer) 1
127.0.0.1:6379> zadd achievements 54 xiaoqing

(2)获取score在指定区间的所有成员

zrangebyscore key min max     # 按score进行从低往高排序获取指定score区间
zrevrangebyscore key min max  # 按score进行从高往低排序获取指定score区间
zrange key start stop         # 按scoer进行从低往高排序获取指定索引区间
zrevrange key start stop      # 按scoer进行从高往低排序获取指定索引区间
zrange achievements 0 -1  # 从低到高全部成员

(3)获取集合长度

zcard key

获取users的长度

zcard achievements

(4)获取指定成员的权重值

zscore key member

获取users中xiaoming的成绩

127.0.0.1:6379> zscore achievements xiaobai
"93"
127.0.0.1:6379> zscore achievements xiaohong
"62"
127.0.0.1:6379> zscore achievements xiaoming
"61"

(5)获取指定成员在集合中的排名

排名从0开始计算

zrank key member      # score从小到大的排名
zrevrank key member   # score从大到小的排名

获取achievements中xiaohei的分数排名,从大到小

127.0.0.1:6379> zrevrank achievements xiaohei
(integer) 4

(6)获取score在指定区间的所有成员数量

zcount key min max

获取achievements从0~60分之间的人数[闭区间]

127.0.0.1:6379> zcount achievements 0 60
(integer) 2
127.0.0.1:6379> zcount achievements 54 60
(integer) 2

(7)给指定成员增加增加权重值

zincrby key score member

给achievements中xiaobai增加10分

127.0.0.1:6379> ZINCRBY achievements 10 xiaobai
"93

(8)删除成员

zrem key member1 member2 member3 ....

从achievements中删除xiaoming的数据

zrem achievements xiaoming

(9)删除指定数量的成员

# 删除指定数量的成员,从最低score开始删除
zpopmin key [count]
# 删除指定数量的成员,从最高score开始删除
zpopmax key [count]

例子:

# 从achievements中提取并删除成绩最低的2个数据
127.0.0.1:6379> zpopmin achievements 2
1) "xiaoqing"
2) "54"
3) "xiaolv"
4) "60"

# 从achievements中提取并删除成绩最高的2个数据
127.0.0.1:6379> zpopmax achievements 2
1) "xiaolan"
2) "99"
3) "xiaobai"
4) "93"

Python操作redis

一、python对redis基本操作

(1)连接redis

# 方式1
import redis

r = redis.Redis(host='127.0.0.1', port=6379)
r.set('foo', 'Bar')
print(r.get('foo'))


# 方式2
import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)
r.set('bar', 'Foo')
print(r.get('bar'))

通常情况下, 当我们需要做redis操作时, 会创建一个连接, 并基于这个连接进行redis操作, 操作完成后, 释放连接,一般情况下, 这是没问题的, 但当并发量比较高的时候, 频繁的连接创建和释放对性能会有较高的影响。于是, 连接池就发挥作用了。连接池的原理是, 通过预先创建多个连接, 当进行redis操作时, 直接获取已经创建的连接进行操作, 而且操作完成后, 不会释放, 用于后续的其他redis操作。这样就达到了避免频繁的redis连接创建和释放的目的, 从而提高性能。

(2)Redis缓存在Django中使用

配置信息

CACHES = {
    # 默认缓存
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        # 项目上线时,需要调整这里的路径
        # "LOCATION": "redis://:密码@IP地址:端口/库编号",
        "LOCATION": "redis://:pwd@ip:port/10",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
        }
    },
    # 提供给admin运营站点的session存储
    "session": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://:密码@IP地址:端口/库编号",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 10},
        }
    },
    # 提供存储短信验证码
    "sms_code": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": ""redis://:密码@IP地址:端口/库编号",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
        }
    }
}
# 设置用户登录admin站点时,记录登录状态的session保存到redis缓存中
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
# 设置session保存的位置对应的缓存配置项
SESSION_CACHE_ALIAS = "session"


---------------------使用-----------------------
from django_redis import get_redis_connection
# 代码示例1
redis = get_redis_connection("sms_code")
code = redis.get(f"sms_{mobile}")

# 代码示例2
pipe = redis.pipeline()
pipe.multi()  # 开启事务
pipe.setex(f"sms_{mobile}", time, code)
pipe.setex(f"interval_{mobile}", sms_interval, "_")
pipe.execute()  # 提交事务,同时把暂存在pipeline的数据一次性提交给redis

(3)数据类型操作

import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0, decode_responses=True)
r = redis.Redis(connection_pool=pool)

# (1)字符串操作:不允许对已经存在的键设置值
ret = r.setnx("name", "yuan")
print(ret)  # False
# (2)字符串操作:设置键有效期
r.setex("good_1001", 10, "2")
# (3)字符串操作:自增自减
r.set("age", 20)
r.incrby("age", 2)
print(r.get("age"))  # b'22'

# (4)hash操作:设置hash
r.hset("info", "name", "rain")
print(r.hget("info", "name"))  # b'rain'
r.hmset("info", {"gedner": "male", "age": 22})
print(r.hgetall("info"))  # {b'name': b'rain', b'gender': b'male', b'age': b'22'}

# (5)list操作:设置list
r.rpush("scores", "100", "90", "80")
r.rpush("scores", "70")
r.lpush("scores", "120")
print(r.lrange("scores", 0, -1))  # ['120', '100', '90', '80', '70']
r.linsert("scores", "AFTER", "100", 95)
print(r.lrange("scores", 0, -1))  # ['120', '100', '95', '90', '80', '70']
print(r.lpop("scores"))  # 120
print(r.rpop("scores"))  # 70
print(r.lindex("scores", 1)) # '95'

# (6)集合操作
# key对应的集合中添加元素
r.sadd("name_set", "zhangsan", "lisi", "wangwu")
# 获取key对应的集合的所有成员
print(r.smembers("name_set"))  # {'lisi', 'zhangsan', 'wangwu'}
# 从key对应的集合中随机获取 numbers 个元素
print(r.srandmember("name_set", 2))
r.srem("name_set", "lisi")
print(r.smembers("name_set"))  # {'wangwu', 'zhangsan'}

# (7)有序集合操作
# 在key对应的有序集合中添加元素
r.zadd("jifenbang", {"yuan": 78, "rain": 20, "alvin": 89, "eric": 45})
# 按照索引范围获取key对应的有序集合的元素
# zrange( name, start, end, desc=False, withscores=False, score_cast_func=float)
print(r.zrange("jifenbang", 0, -1))  # ['rain', 'eric', 'yuan', 'alvin']
print(r.zrange("jifenbang", 0, -1, withscores=True))  # ['rain', 'eric', 'yuan', 'alvin']
print(r.zrevrange("jifenbang", 0, -1, withscores=True))  # ['rain', 'eric', 'yuan', 'alvin']

print(r.zrangebyscore("jifenbang", 0, 100))
print(r.zrangebyscore("jifenbang", 0, 100, start=0, num=1))

# 删除key对应的有序集合中值是values的成员
print(r.zrem("jifenbang", "yuan"))  # 删除成功返回1
print(r.zrange("jifenbang", 0, -1))  # ['rain', 'eric', 'alvin']

# (8)键操作
r.delete("scores")
print(r.exists("scores"))
print(r.keys("*"))
r.expire("name",10)

二、关于redis的实战案例

(1)案例1:KV缓存

第1个是最基础也是最常?的就是KV功能,我们可以用Redis来缓存用户信息、会话信息、商品信息等等。下面这段代码就是通过缓存读取逻辑。

import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=6, decode_responses=True)
r = redis.Redis(connection_pool=pool)


def get_user(user_id):
    user = r.get(user_id)
    if not user:
        user = UserInfo.objects.get(pk=user_id)
        r.setex(user_id, 3600, user)

    return user

(2)案例2:分布式锁

什么是分布式锁

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

提到Redis的分布式锁,很多小伙伴马上就会想到setnx+ expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。

SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。

假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下:

方案1

import redis

pool = redis.ConnectionPool(host='127.0.0.1')
r = redis.Redis(connection_pool=pool)
ret = r.setnx("key_resource_id", "ok")
if ret:
    r.expire("key_resource_id", 5)  # 设置过期时间
    print("抢购成功!")
    r.delete("key_resource_id")  # 释放资源
else:
    print("抢购失败!")

但是这个方案中,setnxexpire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,「别的线程永远获取不到锁啦」

方案2:SETNX + value值是(系统时间+过期时间)

为了解决方案一,「发生异常锁得不到释放的场景」,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。加锁代码如下:

import time


def foo():
    expiresTime = time.time() + 10
    ret = r.setnx("key_resource_id", expiresTime)
    if ret:
        print("当前锁不存在,加锁成功")
        return True

    oldExpiresTime = r.get("key_resource_id")
    if float(oldExpiresTime) < time.time():  # 如果获取到的过期时间,小于系统当前时间,表示已经过期
        # 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        newExpiresTime = r.getset("key_resource_id", expiresTime)
        if oldExpiresTime == newExpiresTime:
            #  考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
            return True  # 加锁成功

    return False  # 其余情况加锁皆失败


foo()

方案3

实际上,我们还可以使用Py的redis模块中的set函数来保证原子性(包含setnx和expire两条指令)代码如下:

r.set("key_resource_id", "1", nx=True, ex=10)

(3)案例4:延迟队列

延时队列可以通过Redis的zset(有序列表)来实现。我们将消息序列化为一个字符串作为zset的值。这个消息的到期时间处理时间作为score,然后用多个线程轮询zset获取到期的任务进行处理,多线程时为了保障可用性,万一挂了一个线程还有其他线程可以继续处理。因为有多个线程,所有需要考虑并发争抢任务,确保任务不能被多次执行。

import time
import uuid

import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379, decode_responses=True)
r = redis.Redis(connection_pool=pool)


def delay_task(task_name, delay_time):
    # 保证value唯一
    task_id = task_name + str(uuid.uuid4())

    retry_ts = time.time() + delay_time
    r.zadd("delay-queue", {task_id: retry_ts})


def loop():
    print("循环监听中...")
    while True:
        # 最多取1条
        task_list = r.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1)

        if not task_list:
            # 延时队列空的,休息1s
            print("cost 1秒钟")
            time.sleep(1)
            continue
        task_id = task_list[0]
        success = r.zrem("delay-queue", task_id)
        if success:
            # 处理消息逻辑函数
            handle_msg(task_id)

def handle_msg(msg):
    """消息处理逻辑"""
    print(f"消息{msg}已经被处理完成!")


import threading

t = threading.Thread(target=loop)
t.start()

delay_task("任务1延迟5", 5)
delay_task("任务2延迟2", 2)
delay_task("任务3延迟3", 3)
delay_task("任务4延迟10", 10)

redis的zrem方法是对多线程争抢任务的关键,它的返回值决定了当前实例有没有抢到任务,因为loop方法可能会被多个线程、多个进程调用, 同一个任务可能会被多个进程线程抢到,通过zrem来决定唯一的属主。同时,一定要对handle_msg进行异常捕获, 避免因为个别任务处理问题导致的循环异常退出。

(4)案例5:发布订阅

subscribe channel # 订阅
publish channel mes # 发布消息
import threading

import redis

r = redis.Redis(host='127.0.0.1')


def recv_msg():
    pub = r.pubsub()

    pub.subscribe("fm104.5")
    pub.parse_response()

    while 1:
        msg = pub.parse_response()
        print(msg)


def send_msg():
    msg = input(">>>")
    r.publish("fm104.5", msg)


t = threading.Thread(target=send_msg)
t.start()

recv_msg()

(5)案例3:定时任务

利用 Redis 也能实现订单30分钟自动取消。

用户下单之后,在规定时间内如果不完成付款,订单自动取消,并且释放库存使用技术:Redis键空间通知(过期回调)用户下单之后将订单id作为key,任意值作为值存入redis中,给这条数据设置过期时间,也就是订单超时的时间启用键空间通知

开启过期key监听

from redis import StrictRedis

redis = StrictRedis(host='localhost', port=6379)

# 监听所有事件
# pubsub = redis.pubsub()
# pubsub.psubscribe('__keyspace@0__:*')
#
# print('Starting message loop')
# while True:
#     message = pubsub.get_message()
#     if message:
#         print(message)

# 监听过期key
def event_handler(msg):
    print("sss",msg)
    thread.stop()

pubsub = redis.pubsub()
pubsub.psubscribe(**{'__keyevent@0__:expired': event_handler})
thread = pubsub.run_in_thread(sleep_time=0.01)

posted @ 2022-03-09 11:54  零哭谷  阅读(154)  评论(0编辑  收藏  举报