如何解决分布式下的session问题?
sudo apt install gcc
tar -zxvf redis-x.x.x.tar.gz
make
make install
whereis
sudo add-apt-repository ppa:redislabs/redis
sudo apt-get update
sudo apt-get install redis
ps -ef |grep redis
whereis redis
只支持Linux版本,因此不考虑在Windows下安装Redis。
redis-server
redis-server /etc/redis/redis.conf
ps -ef | grep redis
redis-cli
ping
底层是双链表(实际上是快速链表),当元素比较少的时候,会使用连续的区域存储,称为压缩链表,当数据很多时,会将多个压缩链表链接起来,叫快速链表
一个字符串列表,单键多值,可以添加元素到头部或尾部
**lpush/rpush **向列表的左端/右端添加一个或多个值
**lpop/rpop **在某个键的左端/右端弹出一个值,会使该值消失,当值都不存在的时候,键也就不存在了
**lrange **查看某个键对应的值,按照索引下标查看该值得某些元素,当start=0,end=-1时,表示查看全部元素
**rpoplpush **从key1右边弹出一个值,压到key2的左边
**lindex **按照index下标获取元素,列表的下标从0开始
**llen **获取某个键对应的值的长度
**linsert before **在value前面插入newvalue
**lrem **从左到右删除n个value
**lset **将key下标为index的值替换成value
bind 127.0.0.1表示,只支持linux本地连接redis,注释掉可以远程连接redis。
表示开启保护模式,yes表示只支持本机访问,no表示可以远程访问。
在timeout秒之内没有对redis操作的时候,redis连接会关闭,默认为0,表示永不超时。
redis是通过tcp连接的,tcp会每tcp-keepalive秒进行一次检测,检测有没有对redis进行操作,如果没有则会关闭连接。
是否允许redis在后台运行,yes表示允许。
redis在运行时,会把端口号设置到一个文件中,这个属性表示该文件的存放路径。
日志级别。
redis数据库的个数。
Redis的发布和订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者接受(sub)消息。客户端可以订阅任意多的频道。
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.7.0version>
dependency>
private static final Jedis JEDIS = new Jedis("192.168.83.75", 6379);
public static void main(String[] args) {
String value = JEDIS.ping();
System.out.println(value);
}
@Test
public void test01() {
// 删除一个键值对
JEDIS.del("k1");
// 添加一个键值对
JEDIS.set("k1", "523");【
// 查询所有key
Set<String> keys = JEDIS.keys("*");
for (String key : keys) {
System.out.println(key);
}
// 查询key的个数
System.out.println(keys.size());
// 判断是否存在某个key
System.out.println(JEDIS.exists("k1"));
// 查看key的存活时间
System.out.println(JEDIS.ttl("k1"));
// 查看key的value
System.out.println(JEDIS.get("k1"));
}
@Test
public void test02() {
JEDIS.del("list1");
// 添加一个列表
JEDIS.lpush("list1", "Lucy", "Mary", "Jack");
// 打印列表
List<String> list1 = JEDIS.lrange("list1", 0, -1);
for (String s : list1) {
System.out.println(s);
}
// 弹出一个值
String name = JEDIS.lpop("list1");
System.out.println(name);
}
@Test
public void test03() {
JEDIS.del("set1");
// 添加一个set集合
JEDIS.sadd("set1", "Lucy", "Mary", "Jack");
// 打印一个Set集合
Set<String> set1 = JEDIS.smembers("set1");
for (String s : set1) {
System.out.println(s);
}
// 删除Set集合中的某个元素
JEDIS.srem("set1", "Lucy");
// 打印删除后的Set集合
Set<String> set2 = JEDIS.smembers("set1");
for (String s : set2) {
System.out.println(s);
}
}
@Test
public void test04() {
JEDIS.del("user");
// 添加记录
Map<String, String> map = new HashMap<>(16);
map.put("name", "Lucy");
map.put("age", "20");
map.put("gender", "female");
JEDIS.hset("user", map);
// 查询记录
String name = JEDIS.hget("user", "name");
System.out.println(name);
}
@Test
public void test05() {
JEDIS.del("zset1");
// 添加记录
Map<String, Double> map = new HashMap<>(16);
map.put("Java", 500.0);
map.put("C++", 400.0);
map.put("Python", 300.0);
JEDIS.zadd("zset1", map);
// 查询记录
Set<String> zset1 = JEDIS.zrange("zset1", 0, -1);
for (String s : zset1) {
System.out.println(s);
}
}
要求:
- 输入手机号,点击发送后随机生成6位验证码,2分钟有效
- 输入验证码,点击验证,返回成功或失败
- 每个手机号每天只能输入3次
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
# Redis服务器地址
spring.redis.host=192.168.83.75
# Redis服务器连接端口
spring.redis.port=6379
# Redis数据库索引
spring.redis.database=0
# Redis连接超时时间(毫秒)
spring.redis.timeout=1800000
# Redis连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
# Redis最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
# Redis连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=5
# Redis连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0
/**
* @author StarKing
*/
@EnableCaching
@Configuration
public class RedisConfig {
/**
* 配置redisTemplate
* 默认情况下的模板只能支持 RedisTemplate,
* 只能存入字符串,很多时候,我们需要自定义 RedisTemplate ,设置序列化器
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
/**
* @author StarKing
*/
@CrossOrigin
@RestController
@RequestMapping("/assemble")
public class AssembleController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/test")
public String test() {
// 设置值到Redis
redisTemplate.opsForValue().set("assemble", "true");
Object assemble = redisTemplate.opsForValue().get("assemble");
return (String) assemble;
}
}
Redis事物是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
从输入Milti命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队过程中,可以通过Discard命令来放弃组队。
乐观锁命令
演示
单独的隔离操作
事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令在没有提交之前都不会被执行,因为事务提交前任何指令都不会被实际执行。
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
private boolean doKill(String userID, String prodID) {
userID = "user_" + userID;
// 商品数量
String prodCount = "prod_" + prodID + "_count";
// 商品清单
String boughtList = "prod_" + prodID + "_bought";
Object o = redisTemplate.opsForValue().get(prodCount);
if (o == null) {
// 秒杀还未开始
System.out.println("秒杀未开始!");
return false;
}
// 秒杀已经开始
// 判断用户有没有重复秒杀
Set<Object> members = redisTemplate.opsForSet().members(boughtList);
if (members != null) {
// 购物清单存在
Boolean isMember = redisTemplate.opsForSet().isMember(boughtList, userID);
if (isMember == null || isMember) {
// 如果用户已经买过,返回false
System.out.println("用户" + userID + "已经完成过秒杀!");
return false;
}
}
int curCount = (int) o;
// 如果已经售空,则返回false
if (curCount <= 0) {
System.out.println("商品已被秒杀完!");
return false;
}
// 开启事务,开始秒杀
// 商品数减1
redisTemplate.opsForValue().decrement(prodCount);
// 添加商品到用户购物车
redisTemplate.opsForSet().add(boughtList, userID);
// 提交事务
System.out.println("用户" + userID + "秒杀成功!");
return true;
}
存在的问题
在并发的情况下,会出现超卖问题。同时Redis有可能无法处理多个连接请求,因此会出现连接超时问题。
超卖和超时问题的解决
超卖问题可以采用同步机制解决
超时问题可以用Redis连接池来解决
RDB是什么?
在指定的时间间隔内将内存中的数据集快照写入磁盘(Snapshot快照文件),恢复的时候,将快照文件读入到内存。
如何执行?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件(dump.rdb)。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失。
RDB相关配置
RDB的优点
RDB的缺点
如何停止持久化?
动态停止RDB:redis-cli config set save “”,save后给空值,表示禁用保存策略
RDB的备份
拷贝rdb文件。
AOF是什么?
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话,就根据日志文件袋内容将写指令,从前到后执行一次,以完成数据的恢复工作。
AOF持久化流程
AOF默认不开启
修改redis.conf文件中的appendonly为yes,开启AOF;
可以在redis.conf中配置文件名称,默认为appendonly.aof;
AOF的保存路径与RDB的路径一致;
重启后配置生效。
AOF和RDB同时开启,Redis采用什么策略?
系统默认取AOF的数据,因为数据不会存在丢失。
异常恢复
AOF同步频率设置
rewrite重写压缩
主机数据更新后,根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。一主多从。
好处:
读写分离:主服务器只进行写操作,从服务器只进行读操作。
容灾的快速恢复:当某一台从服务器挂掉的时候,可以从其他的从服务器读取数据。
一主两从
薪火相传
反客为主
某个主服务器宕机时,可以在从机上通过slaveof no one命令将该从机设置为主机。
搭建一个三主三从的集群,主机端口号为6379、6380、6381,从机端口号为6389、6390、6391。
当然在工作中,每个Redis服务都会各自占用一台服务器。
编写redis6379.conf
复制其他配置文件
配置每个文件中的端口号等属性
启动六个Redis服务
将六个节点合成集群
redis-cli --cluster create --cluster-replicas 1 172.25.191.198:6379 172.25.191.198:6380 172.25.191.198:6381 172.25.191.198:6389 172.25.191.198:6390 172.25.191.198:6391
其中,–cluster-replicas表示选择搭建方式,1表示以最简单的方式搭建集群。
如果有以上的效果,表示已经完成了三主三从的集群搭建。
连接集群,进行测试
使用命令**redis-cli -c -p **连接集群。
使用cluster nodes查看集群信息。
搭建集群时,成功后会显示一句**[OK] All 16384 slots covered**,这里的16384就是slot的个数,数据库中的每一个键都属于这16384个插槽的其中一个。
集群使用公式**CRC16(key) %**来计算键key属于哪个槽。
集群中每个节点负责处理一部分插槽,例如A节点负责0-5460,B节点负责5461-10922,C节点负责10923-16383。当使用set命令向数据库中插入值的时候,会用这个set的key先进行计算,然后根据计算结果,确定在哪个服务器中加入数据。
主机挂掉之后,从机会上位。主机重启后,会变为从机。
如果主从机都挂掉了,集群能否正常工作取决于cluster-require-full-coverage,如果该值为yes,表示某个节点挂掉之后,整个集群会瘫痪;如果该值为no,表示只有该节点不能提供服务。
/**
* @author StarKing
*/
public class ColonyTest {
public static void main(String[] args) {
// 端口号可以是任意一个,因为是无中心化的
HostAndPort hostAndPort = new HostAndPort("172.25.191.198", 6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
// jedisCluster包含了所有的操作
jedisCluster.set("k1", "v1");
}
}
问题描述
单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下单并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题,就需要一种跨JVM的互斥机制来控制共享资源的访问。
解决方案:
使用Redis实现分布式锁
代码实现
@CrossOrigin
@RestController
@RequestMapping
public class DistributedController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/request")
public String request() throws InterruptedException {
// setnx获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "10", 3, TimeUnit.SECONDS);
if (lock != null && lock) {
// 如果获取到锁
// 执行操作
System.out.println("执行了相关操作!");
// 释放锁
redisTemplate.delete("lock");
} else {
// 如果没有获取到锁,即锁被其他线程占用
TimeUnit.SECONDS.sleep(1);
return request();
}
return "";
}
}
上面的代码存在锁误删问题:假如有两台服务器,第一台获取到锁之后,进行了一段时间的卡顿,假如卡顿了4秒,而锁的存活时间为3秒,因此在第3秒的时候,锁被释放,此时B服务器获取到了该锁,需要后续进行2秒的操作,但是操作到第1秒的时候,A服务器将锁手动释放掉,也就是说此时B服务器的锁就会被释放。
锁误删问题解决方案:使用UUID解决,保证每个服务器释放的都是自己的锁,即释放锁的时候进行判断,判断当前的UUID和要释放掉UUID是否一致,如果一致才释放,需要注意的是,必须保证这个判断是一个原子操作。
那么如何实现该判断是原子操作?我们引入LUA脚本,LUA脚本是一款嵌入式语言,它的操作可以保证原子性。由此我们得到最终版本:
@CrossOrigin
@RestController
@RequestMapping
public class DistributedController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/request")
public String request() throws InterruptedException {
// setnx获取锁
String uuid = UUID.randomUUID().toString();
// 商品ID
int prodId = 10;
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock" + prodId, uuid, 3, TimeUnit.SECONDS);
if (lock != null && lock) {
// 如果获取到锁
// 执行操作
System.out.println("执行了相关操作!");
// 使用LUA脚本释放锁
String lua = "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(lua);
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Collections.singletonList("lock" + prodId), uuid);
} else {
// 如果没有获取到锁,即锁被其他线程占用
TimeUnit.SECONDS.sleep(1);
return request();
}
return "true";
}
}