redis基础
redis是常用的非关系型数据库、缓存数据库,nosql技术的一种代表,主要存储以key-value对为主,key均为String,读写速度非常快,据说是可以达到10w并发。支持数据持久化。它属于单线程服务,但这不影响它的高并发特性。
redis使用systemed部署安装
- cp redis.conf /etc/ 备份一份配置文件用来运行程序
- vi /etc/redis.conf #将daemonize no改为daemonize yes 开启守护进程(系统服务)
- redis-server /etc/redis.conf 运行
如果之前启动过可以查看端口占用把之前的服务kill掉
lsof -i:端口号(默认是6379)
或者 netstat -tunlp|grep 端口号
然后 kill -9 pid(占用的进程id)
编写systemed守护进程
cp redis-server /usr/local/bin/ **复制一份启动用**
vim /usr/lib/systemd/system/redis.service
##内容如下
[Unit]
Description=Redis
After=network.target
[Service]
Type=forking
PIDFile=/var/run/redis_6379.pid
ExecStart=/usr/local/bin/redis-server /etc/redis.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
##到此结束
ln -s /usr/lib/systemd/system/redis.service /etc/systemd/system/multi-user.target.wants/redis.service
systemctl daemon-reload
systemctl start redis
redis五大基本数据类型(value)
- list
- string
- set
- Hash
- sort set 又叫zset
redis新增三大数据类型
- Bitmaps 主要进行位操作的字符串
setbit -key -offset -value (偏移量过大可能造成redis阻塞)
getbit -key -offset
bitcount -key 计算值为1的比特位数量
bitop and\or\not\xor result key1 key2 位运算
- HyperLogLog 主要用于基数统计
pfadd -key element…
pfcount -key 统计基数数量
pfmerge -result -key1 -key2 合并
- Geospatial 地理位置信息
geoadd -key -经度 -维度 -valueName…
geopos -key -valuename
计算距离 geodist -key -valueName1 -valueName2 -单位(km、m)
计算周边坐标 georadius -value -经度 -维度 -距离 -单位
Jedis操作redis
1. jedis中对键通用的操作
方法 | 描述 | 返回值 /补充说明 |
---|---|---|
jedis.flush | ||
jedis.flushDB 清空数据 | ||
boolean jedis.exists(String key) | 判断某个键是否存在 | true = 存在,false= 不存在 |
jedis.set(String key,String value) | 新增键值对(key,value) | 返回String类型的OK代表成功 |
Set jedis.keys(*) | 获取所有key | 返回set 无序集合 |
jedis.del(String key) | 删除键为key的数据项 | |
jedis.expire(String key,int i) | 设置键为key的过期时间为i秒 | |
int jedis.ttl(String key) | 获取建委key数据项的剩余时间(秒) | |
jedis.persist(String key) | 移除键为key属性项的生存时间限制 | |
jedis.type(String key) | 查看键为key所对应value的数据类型 |
2. jedis中字符串的操作
字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这 便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等。 在Redis中字符串类型的Value最多可以容纳的数据长度是512M。
语法 | 描述 |
---|---|
jedis.set(String key,String value) | 增加(或覆盖)数据项 |
jedis.setnx(String key,String value) | 不覆盖增加数据项(重复的不插入) |
jedis.setex(String ,int t,String value) | 增加数据项并设置有效时间 |
jedis.del(String key) | 删除键为key的数据项 |
jedis.get(String key) | 获取键为key对应的value |
jedis.append(String key, String s) | 在key对应value 后边扩展字符串 s |
jedis.mset(String k1,String V1,String K2,String V2…) | 增加多个键值对 |
String[] jedis.mget(String K1,String K2,…) | 获取多个key对应的value |
jedis.del(new String[](String K1,String K2,… )) | 删除多个key对应的数据项 |
String jedis.getSet(String key,String value) | 获取key对应value并更新value |
String jedis.getrang(String key , int i, int j) | 获取key对应value第i到j字符 ,从0开始,包头包尾 |
3. jedis中对整数和浮点数操作
语法 | 描述 |
---|---|
jedis.incr(String key) | 将key对应的value 加1 |
jedis.incrBy(String key,int n) | 将key对应的value 加 n |
jedis.decr(String key) | 将key对应的value 减1 |
jedis.decrBy(String key , int n) | 将key对应的value 减 n |
4. jedis中对列表(list)操作
在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表 一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是 4294967295。
从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将 会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的。
语法 | 描述 |
---|---|
jedis.lpush(String key, String v1, String v2,…) | 添加一个List , 注意:如果已经有该List对应的key, 则按顺序在左边追加 一个或多个 |
jedis.rpush(String key , String vn) | key对应list右边插入元素 |
jedis.lrange(String key,int i,int j) | 获取key对应list区间[i,j]的元素,注:从左边0开始,包头包尾 |
jedis.lrem(String key,int n , String val) | 删除list中 n个元素val |
jedis.ltrim(String key,int i,int j) | 删除list区间[i,j] 之外的元素 |
jedis.lpop(String key) | key对应list ,左弹出栈一个元素 |
jedis.rpop(String key) | key对应list ,右弹出栈一个元素 |
jedis.lset(String key,int index,String val) | 修改key对应的list指定下标index的元素 |
jedis.llen(String key) | 获取key对应list的长度 |
jedis.lindex(String key,int index) | 获取key对应list下标为index的元素 |
jedis.sort(String key) | 把key对应list里边的元素从小到大排序 (后边详细介绍) |
5. jedis 集合set 操作
在Redis中,我们可以将Set类型看作为没有排序的字符集合,和List类型一样,也可以在该类型的数据值上执行添加、删除或判断某一元素是否存在等操作。需要 说明的是,这些操作的时间是常量时间。Set可包含的最大元素数是4294967295。
和List类型不同的是,Set集合中不允许出现重复的元素。和List类型相比,Set类型在功能上还存在着一个非常重要的特性,即在服务器端完成多个Sets之间的聚合计 算操作,如unions、intersections和differences(就是交集并集那些了)。由于这些操作均在服务端完成, 因此效率极高,而且也节省了大量的网络IO开销
语法 | 操作 |
---|---|
jedis.sadd(String key,String v1,String v2,…) | 添加一个set |
jedis.smenbers(String key) | 获取key对应set的所有元素 |
jedis.srem(String key,String val) | 删除集合key中值为val的元素 |
jedis.srem(String key, Sting v1, String v2,…) | 删除值为v1, v2 , …的元素 |
jedis.spop(String key) | 随机弹出栈set里的一个元素 |
jedis.scared(String key) | 获取set元素个数 |
jedis.smove(String key1, String key2, String val) | 将元素val从集合key1中移到key2中 |
jedis.sinter(String key1, String key2) | 获取集合key1和集合key2的交集 |
jedis.sunion(String key1, String key2) | 获取集合key1和集合key2的并集 |
jedis.sdiff(String key1, String key2) | 获取集合key1和集合key2的差集 |
6. jedis中 有序集合Zsort
Sorted-Sets和Sets类型极为相似,它们都是字符串的集合,都不允许重复的成员出现在一个Set中。它们之间的主要差别是Sorted-Sets中的每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。然 而需要额外指出的是,尽管Sorted-Sets中的成员必须是唯一的,但是分数(score) 却是可以重复的。
在Sorted-Set中添加、删除或更新一个成员都是非常快速的操作,其时间复杂度为集合中成员数量的对数。由于Sorted-Sets中的成员在集合中的位置是有序的,因此,即便是访问位于集合中部的成员也仍然是非常高效的。事实上,Redis所具有的这一特征在很多其它类型的数据库中是很难实现的,换句话说,在该点上要想达到和Redis同样的高效,在其它数据库中进行建模是非常困难的。
例如:游戏排名、微博热点话题等使用场景。
语法 | 描述 |
---|---|
jedis.zadd(String key,Map map) | 添加一个ZSet |
jedis.hset(String key,int score , int val) | 往 ZSet插入一个元素(Score-Val) |
jedis.zrange(String key, int i , int j) | 获取ZSet 里下表[i,j] 区间元素Val |
jedis. zrangeWithScore(String key,int i , int j) | 获取ZSet 里下表[i,j] 区间元素Score - Val |
jedis.zrangeByScore(String , int i , int j) | 获取ZSet里score[i,j]分数区间的元素(Score-Val) |
jeids.zscore(String key,String value) | 获取ZSet里value元素的Score |
jedis.zrank(String key,String value) | 获取ZSet里value元素的score的排名 |
jedis.zrem(String key,String value) | 删除ZSet里的value元素 |
jedis.zcard(String key) | 获取ZSet的元素个数 |
jedis.zcount(String key , int i ,int j) | 获取ZSet总score在[i,j]区间的元素个数 |
jedis.zincrby(String key,int n , String value) | 把ZSet中value元素的score+=n |
7. jedis中 哈希(Hash)操作
Redis中的Hashes类型可以看成具有String Key和String Value的map容器。所以该类型非常适合于存储值对象的信息。如Username、Password和Age等。如果Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。每一个Hash可以存储4294967295个键值对。
语法 | 描述 |
---|---|
jedis.hmset(String key,Map map) | 添加一个Hash |
jedis.hset(String key , String key, String value) | 向Hash中插入一个元素(K-V) |
jedis.hgetAll(String key) | 获取Hash的所有(K-V) 元素 |
jedis.hkeys(String key) | 获取Hash所有元素的key |
jedis.hvals(String key) | 获取Hash所有元素的value |
jedis.hincrBy(String key , String k, int i) | 把Hash中对应的k元素的值 val+=i |
jedis.hdecrBy(String key,String k, int i) | 把Hash中对应的k元素的值 val-=i |
jedis.hdel(String key , String k1, String k2,…) | 从Hash中删除一个或多个元素 |
jedis.hlen(String key) | 获取Hash中元素的个数 |
jedis.hexists(String key,String K1) | 判断Hash中是否存在K1对应的元素 |
jedis.hmget(String key,String K1,String K2) | 获取Hash中一个或多个元素value |
8. 排序操作(不改变原始数据)
使用排序, 首先需要生成一个排序对象
SortingParams sortingParams = new SortingParams();
语法 | 描述 |
---|---|
jedis.sort(String key,sortingParams.alpha()) | 队列按首字母a-z 排序 |
jedis.sort(String key, sortingParams.asc() ) | 队列按数字升序排列 |
jedis.sort(String key , sortingParams.desc()) | 队列按数字降序排列 |
redis中的事务和锁机制
redis的事务:是一个单独的隔离操作,串联多个命令,阻塞后统一执行,防止其他命令插队,不支持回滚。
- Multi 命令队列
- Exec 执行命令
- discard 放弃执行
如果队列中有编写期错误则会放弃整个队列的执行,如果是运行时错误则只报错错误的命令,其他命令正常执行
redis事务冲突的解决
- 悲观锁
在当前操作的时候,其他线程不能进来操作(阻塞在外面),认为每次操作都不安全,进行加锁操作 例如:行锁、表锁、读锁、写锁
- 乐观锁
给需要操作的数据加上字段version,每次修改前check—and-set,如果版本不对则不操作,每次修改同时修改version, 场景:抢票
redis通过watch字段实现乐观锁
watch key (在Multi之前执行)
unwatch key 取消监视
如果key值变动则会返回 nil
redis事务三特性
- 单独的隔离操作,单线程串行执行
- 没有隔离级别概念,队列中未提交之前都不会执行
- 不保证原子性,执行期错误不影响后续命令继续执行(watch监视到的变化属于非执行期错误)
2.redis实现简单秒杀案例
可能出现问题:超卖、连接超时
- 乐观锁 -> 解决超卖问题 -> 可能造成库存遗留问题
- 连接池 -> 解决连接超时问题
lua脚本可以写多步redis操作,代替Redis中的事务使用,具有一定的原子性,不会被其他命令插队,适合用lua脚本可以解决库存遗留问题(乐观锁变成悲观锁)
/**
* 秒杀案例
* 维护两个表
* 表一:商品剩余个数 -1
* 表二:秒杀成功名单 +1
*/
//秒杀过程
public static boolean doSecKill(String uid,String proId){
// 1.uid和proId的非空判断
if (null == uid || null == proId){
return false;
}
//2.连接redis
// Jedis jedis = new Jedis(hostName,6379);
// jedis.auth(password);
// 使用连接池
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3.拼接key
// 3.1 库存key
String kcKey = "sk:" + proId + ":qt";
// 3.2 秒杀成功的用户set的key
String userKey = "sk:" + proId + ":user";
/**
* 监视库存key
* 乐观锁实现
*/
jedis.watch(kcKey);
// 4.秒杀是否开始:获取库存,如果库存为null,说明秒杀还没开始
String kc = jedis.get(kcKey);
if (null == kc){
System.out.println("秒杀还没开始,请等待");
jedis.close();
return false;
}
// 5.开始秒杀:判断用户是否重复进行秒杀
if (jedis.sismember(userKey, uid)){
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
// 6.秒杀结束判断:判断商品数量,粗存小于1,秒杀结束
if(Integer.parseInt(kc) <= 0){
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
// 7.秒杀过程:
// 使用事务
Transaction multi = jedis.multi();
multi.decr(kcKey);
multi.sadd(userKey,uid);
List<Object> results = multi.exec();
if (results == null || results.size() == 0){
System.out.println("秒杀失败了。。。。。。");
jedis.close();
return false;
}
// // 7.1 库存-1
// jedis.decr(kcKey);
// // 7.2 把用户放入到秒杀成功的清单内
// jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
return true;
}
使用lua脚本实现
static String secKillScript ="local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":usr\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1" ;
static String secKillScript2 =
"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
" return 1";
public static boolean doSecKill(String uid,String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
//String sha1= .secKillScript;
String sha1= jedis.scriptLoad(secKillScript);
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
redis中的持久化方式
- RDB -特定的算法按时间间隔生成快照文件保存到硬盘
默认规则:3600 1 300 100 60 1000 多少秒内多少key发生变化,持久化一次,redis会fork一个子进程去先持久化写入到临时文件中,然后替换之前的rdb文件(写时复制技术)。缺点:最后一次持久化的数据可能丢失,写时拷贝可能会两倍膨胀空间。默认名dump.rdb
- AOF -向文件中以日志的形式把所有写操作指令记录下来
默认不开启,开启之后,系统恢复数据取AOF数据,默认名 appendonly.aof。
异常恢复:redis-check-aof–fix可以对appendonly.aof 进行恢复。
appendfsync 同步频率 always/everysec/no
Rewrite 重写压缩 大于文件大小基础值的两倍时开始重写
缺点:空间占用更大,备份恢复慢,每次读写同步性能压力,存在个别bug使得恢复不能。
主从复制与集群
主从复制
主机数据更新后自动同步到备机 master/slave机制,master以写为主,slave以读为主,slave会从master同步(获取主服务器的rdb文件-全量复制),后面master会主动传输新增修改命令给slave(增量复制)。作用:读写分离(主机写入,从机读取)、容灾快速恢复。
- 一主多从
info Replication 查看主从情况命令
slaveof -ip -port 从机命令(通过命令设置关机后重开失效,写入配置文件则重开依然有效)
拷贝多个redis.conf文件include(写绝对路径)
开启daemonize yes
Pid文件名字pidfile
指定端口port
Log文件名字
dump.rdb名字dbfilename
Appendonly 关掉或者换名字
从机配置信息
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
- 薪火相传 :从服务器同时作为别的服务器的主服务器被同步(类树形结构)
- 反客为主 :当master宕机,slave立刻升级为master,后面的slave不用做任何修改。
slaveof no one 可以把从机变成主机
- 哨兵模式(sentinel):反客为主的自动版。
配置文件sentinel.conf
sentinel monitor mymaster 127.0.0.1 6379 1
# 其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
redis.conf里 replica-priority 值越小优先级越高
选择规则:
- 选择优先级靠前的
- 选择偏移量最大的
- 选择runid最小的服务
启动哨兵 sentinel sentinel.conf
缺点:复制延迟
集群
redis容量不够时如何扩容,redis并发写时如何分担
解决ip地址变化
- 代理主机方式 (由代理主机决定请求哪台服务器)
- 无中心化集群 (任何一台服务器都可以作为集群的入口,之间互相连通)
基本配置
开启daemonize yes
Pid文件名字
指定端口
Log文件名字
Dump.rdb名字
Appendonly 关掉或者换名字
集群配置
cluster-enabled yes 打开集群模式
cluster-config-file nodes-6379.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
配置文件
include /home/bigdata/redis.conf
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
合并redis服务形成集群(确认都生成了node文件后)
redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391
集群方式给连接redis: redis-cli -c -p 端口号
通过分配插槽slot分配不同的主机存储不同的key,hash(key) = 插槽值
主机挂掉,从机变主机,主机恢复,主机变从机
jedis操作集群
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.31.211",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
redis集群的优点:
- 实现扩容
- 分摊压力
- 无中心化配置相对简单
缺点:
- 不支持多键操作(可以按组添加)
- 多键的redis事务不被支持
- lua脚本不支持
- 出现较晚
redis应用
1.随机生成6位验证码,两分钟之内有效,每个手机号每天只能发送三次验证码
public static String hostName = "xxxx";
public static String password = "xxxx";
// 随机生成6位验证码,两分钟之内有效,每个手机号每天只能发送三次验证码
//1. 生成6位数字验证码
public String getCode(){
Random random = new Random();
String code = "";
for (int i = 0; i < 6; i++) {
// 生成10以内的值
code += random.nextInt(10);
}
return code;
}
//2.每个手机每天只能发送三次请求验证码,验证码放到redis里去
public void verifyCode(String phone){
Jedis jedis = new Jedis(hostName,6379);
jedis.auth(password);
// 手机发送次数的key
String countKey = "VerifyCode" + phone + ":count";
// 验证码的key
String codeKey = "VerifyCode" + phone + ":code";
// 每个手机每天只能发三次
String count = jedis.get(countKey);
if (count == null){
// 第一次发送
jedis.setex(codeKey,24*60*60L,"1");
}else if(Integer.parseInt(count) <= 2){
// 发送次数+1
jedis.incr(codeKey);
}else if(Integer.parseInt(count) > 2){
// 发送过三次
System.out.println("今天已经发送过三次了!");
}
// 发送验证码到redis内
String verifyCode = getCode();
jedis.setex(codeKey,120L,verifyCode);
jedis.close();
}
// 3. 验证码校验
public void getRedisCode(String phone,String code){
Jedis jedis = new Jedis(hostName,6379);
jedis.auth(password);
// 验证码的key
String codeKey = "VerifyCode" + phone + ":code";
String redisCode = jedis.get(codeKey); // 过期则返回null
// 判断验证码是否正确
if(null != redisCode && redisCode.equals(code)){
System.out.println("成功!");
}else {
System.out.println("失败!");
}
}
redis应用中的问题
1. 缓存穿透问题
缓存穿透:1.应用服务器压力变大 2.redis命中率变低 3.一直查询数据库
原因:1. redis查不到数据库 2. 出现很多非正常url访问(可能遭受恶意攻击)
解决方案:
- 对空值做缓存:如果一个查询结果为空(无论数据是否存在),我们任然把这个空结果(null)进行缓存(下次有相同请求可以从缓存里返回),设置空结果的过期时间很短,最长不超过5分钟。
- 设置课访问的名单(白名单模式) :使用bitmaps类型定义一个可访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里的id做比较,如果id不在bitmaps里则进行拦截。
- 采用布隆过滤器:本质上时一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数) 底层就是bitmaps
- 进行实时监控(黑名单模式):当发现redis的命中率开始急速降低,需要排查访问对象和访问的数据,运维人员设置黑名单服务。
2. 缓存击穿问题
缓存击穿现象:1.数据库访问压力瞬时增大 2.redis里没有出现key过期 3.redis正常运行
问题:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,超高并发量访问
解决方案:
- 预先设置热门数据:在redis高峰访问之前,把一些热门数据预先存到redis里,加大这些热门数据key的时长
- 实时调整:现场监控哪些数据热门,实时调整key的过期时长。
- 使用锁:
- 加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
- 可应用分布式锁,保证只有一个请求会走到数据库
- 也可以考虑jvm锁,jVM 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好。
3. 缓存雪崩
缓存雪崩: 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
- 构建多级缓存架构: nginx缓存 + redis缓存 + 其他缓存(ehcache等)
- 使用锁或者队列: 用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用于高并发场景
- 设置缓存标志更新缓存: 记录缓存数据是否过期(设置提前量),如果过期触发通知另外的线程在后台去更新实际的key的缓存。
- 将缓存失效时间分散开: 原有的失效时间 + 随机值 = 设置的失效时间,减少集体失效的情况发生。
4. 分布式锁
分布式锁:解决多机问题中跨jvm的互斥机制来限制贡献资源的访问。
实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于zookeeper
每一种分布式锁都有其特点:
- 性能最高:redis
- 可靠性最高:zookeeper
setnx key value # 加上redis分布式锁的命令(悲观锁)
del key # 释放上锁的key -> 手动释放锁
expire key # 设置过期时间 -> 自动释放锁
set key value nx ex 过期时间 # 一边上锁(nx),一边设置过期时间(ex)
设置分布式锁
@GetMapping("testLock")
public void testLock(){
//1获取锁,setne
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
String lockUuid = (String)redisTemplate.opsForValue().get("lock");
if(lockUuid.equals(uuid)){
redisTemplate.delete("lock");
}
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用lua脚本,保证操作的原子性
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
满足分布式锁可用的特性:
- 互斥性:任何时候只有一个客户端能持有锁
- 不会发生死锁:即使一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能枷锁。
- 解铃还须系铃人:加锁和解锁必须是同一个人,不能把别人的锁解了
- 加锁和解锁操作必须具有原子性
redis 6.0新功能
- ACL 访问控制列表
acl list 显示所有用户和当前权限
- IO多线程
- 命令执行依旧是单线程
- 客户端交互部分的网络IO模块变成多线程,默认不开启
- 工具支持Cluster
集成ruby环境