主题
缓存淘汰策略、Redis事务、乐观锁
目标
- 理解缓存淘汰的LRU策略
- 理解Redis事务的应用
- 利用事务实现乐观锁的实现
缓存淘汰策略
LRU原理
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表
保存缓存数据,详细算法实现如下:
- 1、新数据插入到链表头部;
- 2、每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 3、当链表满的时候,将链表尾部的数据丢弃。
在Java中可以使用LinkHashMap去实现LRU
- 利用哈希链表实现
案例分析
- 让我们以用户信息的需求为例,来演示一下LRU算法的基本思路:
- 1.假设我们使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个用户是按照时间顺序依次从链表右端插入的。
- 2.此时,业务方访问用户5,由于哈希链表中没有用户5的数据,我们从数据库中读取出来,插入到缓存当中。这时候,链表中最右端是最新访问到的用户5,最左端是最近最少访问的用户1。
- 3.接下来,业务方访问用户2,哈希链表中存在用户2的数据,我们怎么做呢?我们把用户2从它的前驱节点和后继节点之间移除,重新插入到链表最右端。这时候,链表中最右端变成了最新访问到的用户2,最左端仍然是最近最少访问的用户1。
- 4.接下来,业务方请求修改用户4的信息。同样道理,我们把用户4从原来的位置移动到链表最右侧,并把用户信息的值更新。这时候,链表中最右端是最新访问到的用户4,最左端仍然是最近最少访问的用户1。
- 5.后来业务方换口味了,访问用户6,用户6在缓存里没有,需要插入到哈希链表。假设这时候缓存容量已经达到上限,必须先删除最近最少访问的数据,那么位于哈希链表最左端的用户1就会被删除掉,然后再把用户6插入到最右端。
- 以上,就是LRU算法的基本思路。
- 参考https://www.itcodemonkey.com/article/11153.html
Redis缓存淘汰策略
设置最大缓存
-
在 redis 中,允许用户设置最大使用内存大小
maxmemory
(redis.config里配置),默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。 -
redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
淘汰策略
-
redis淘汰策略配置:maxmemory-policy voltile-lru(redis.config里配置),支持热配置
-
redis 提供 6种数据淘汰策略:
- 1、 volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- 2、volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- 3、volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- 4、allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- 5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- 6、no-enviction(驱逐):禁止驱逐数据
Redis事务
Redis事务介绍
- Redis 的事务是通过
MULTI 、 EXEC 、 DISCARD 和 WATCH
这四个命令来完成的。 - Redis 的
单个命令都是原子性
的,所以这里需要确保事务性的对象是命令集合。 - Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
- Redis 不支持回滚操作。
监控的key发生变化,同一个事务内的所有命令都不会执行
事务命令
- MULTI
- 用于标记事务块的开始。
Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。 - 语法:multi
- 用于标记事务块的开始。
- EXEC
- 在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态
- 语法:exec
- DISCARD
- 清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
- 语法:discard
- WATCH
- 当某个[事务需要按条件执行]时,就要使用这个命令将给定的[键设置为受监控]的状态。
- 语法:watch key [key…]
- 注意事项:使用该命令可以实现 Redis 的乐观锁。(后面实现)
- UNWATCH
- 清除所有先前为一个事务监控的键。
- 语法:unwatch
事务演示
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 111
QUEUED
127.0.0.1:6379> hset set1 name zhangsan
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s2 222
QUEUED
127.0.0.1:6379> hset set2 age 20
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> watch s1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 555
QUEUED
127.0.0.1:6379> exec # 此时在没有exec之前,通过另一个命令窗口对监控的s1字段
进行修改
(nil)
127.0.0.1:6379> get s1
111
事务失败处理
Redis 语法错误
- 整个事务的命令在队列里都清除
Redis 运行错误
-
在队列里正确的命令可以执行 (弱事务性)
- 弱事务性 :
- 1、
在队列里正确的命令可以执行
(非原子操作) - 2、
不支持回滚
- 1、
- 弱事务性 :
Redis 不支持事务回滚(为什么呢)
- 1、大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
- 2、 Redis 为了性能方面就忽略了事务回滚。 (回滚记录历史版本)
乐观锁
乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试
,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。具体思路如下:
- 1、利用redis的watch功能,监控这个redisKey的状态值
- 2、获取redisKey的值
- 3、创建redis事务
- 4、给这个key的值+1
- 5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1
public void watch() {
try {
String watchKeys = "watchKeys";
//初始值 value=1
jedis.set(watchKeys, 1);
//监听key为watchKeys的值
jedis.watch(watchkeys);
//开启事务
Transaction tx = jedis.multi();
//watchKeys自增加一
tx.incr(watchKeys);
//执行事务,如果其他线程对watchKeys中的value进行修改,则该事务将不会执行
//通过redis事务以及watch命令实现乐观锁
List<Object> exec = tx.exec();
if (exec == null) {
System.out.println("事务未执行");
} else {
System.out.println("事务成功执行,watchKeys的value成功修改");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
Redis乐观锁实现秒杀
-
在生产环境里,经常会利用redis乐观锁来实现秒杀,Redis乐观锁是Redis事务的经典应用。
-
秒杀场景描述:
- 秒杀活动对稀缺或者特价的商品进行定时,定量售卖,吸引成大量的消费者进行抢购,但又只有少部分消费者可以下单成功。因此,秒杀活动将在较短时间内产生比平时大数十倍,上百倍的页面访问流量和下单请求流量。
-
由于秒杀只有少部分请求能够成功,而大量的请求是并发产生的,所以如何确定哪个请求成功了,就是由redis乐观锁来实现。具体思路如下:
- 监控 锁定量,如果该值被修改成功则表示该请求被通过,反之表示该请求未通过。
- 从监控到修改到执行都需要在redis里操作,这样就需要用到Redis事务。
-
为什么用redis实现秒杀?
因为redis是单进程单线程,没有并发问题。其次,单个命令是原子操作
。
public class SecKill {
public static void main(String[] arg) {
//库存key
String redisKey = "stock";
ExecutorService executorService = Executors.newFixedThreadPool(20);
try {
Jedis jedis = new Jedis("127.0.0.1", 6378);
// 已经被秒杀的库存的初始值,从0开始
jedis.set(redisKey, "0");
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
Jedis jedis1 = new Jedis("127.0.0.1", 6378);
try {
jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
String userInfo = UUID.randomUUID().toString();
// 假设库存总共20个,小于20则没有秒完
if (valInteger < 20) {
Transaction tx = jedis1.multi();
tx.incr(redisKey);
List list = tx.exec();
// 秒成功 失败返回空list而不是空
if (list != null && list.size() > 0) {
System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (valInteger + 1));
// 版本变化,被别人抢了。
}else {
System.out.println("用户:" + userInfo + ",秒杀失败");
}
}else {
// 秒完了
System.out.println("已经有20人秒杀成功,秒杀结束");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis1.close();
}
});
}
executorService.shutdown();
}
}