Redis

Redis

应用环境

分布式项目微服务框架下,不同tomcat怎么共享session(session只存在一个tomcat之中),

虽然tomcat本身具有广播的功能,但是由于tomcat每个都要指定其他所有共享的tomcat目标,这种配置很繁琐,而且耗费tomcat性能,运用Redis就像运用一个第三方的存储,存放当前的登录信息,可以有效解决这样的问题

单点登录

一级域名单点登录:不同系统的单点登录,OA系统登录,商城系统登录也有效

二级域名单点登录:一个系统多个功能,其中一个登录了,另外功能也登录状态

NoSql

非关系型数据库,不同于关系型数据库,没有指定列的类型,速度快(无需维护关系)

  • 文档型:mongodb
  • 键值对型:redis 数据防止在内存,速度快100倍(1s:11万次读,8万次写)

Redis秒杀缓解解析

redis因为其操作内存的原因,运用缓存速度快的特点,用作缓存服务器它可以用于减轻数据库压力.对比memcache,reid可以写入到文件,保证了数据的完整.比如出现秒杀业务的时候,先查询出余量,写入到redis中,每次发生购买,就修改redis的余量信息,当业务压力缓解的时候,再把数据写入到关系数据库(关系数据库可靠)中.

Redis分布式锁

通常java同步锁,只能锁一个java虚拟机的环境中的方法

通过redis锁,可以把这个锁放在redis中,当分布式环境下的该方法要被调用,先查看redis中的锁是或否被使用,再判断是否继续或者等待

Redis安装

  1. 拷贝文件

  2. 解压文件

  3. tar -zxf redis
    
  4. 进入redis复制redis.conf到src

    cp redis.conf src/
    
  5. 修改src下的redis.conf,将daemonize no改为daemonize yes

    vi redis.conf
    
  6. 启动redis服务

    一般方式启动redis不能ctrl+c,否则会立刻退出,这里通过添加守护进程的方式保证redis长服务,守护进程是daemon yes

    ./redis-server redis.conf
    
  7. 运行客户端

    ./redis-cli
    
  8. 开放端口

    firewall-cmd --zone=public --add-port=6379/tcp --permanent
    firewall-cmd --reload
    
  9. 允许远程连接 修改redis.conf,注释 bind和内容

    #bind 127.0.0.1
    
  10. 如果是测试用,可以将redis设置非保护模式,同时注释密码

protect-mode no

Redis数据类型

String(字符串)

string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个value。

Hash(哈希)

Redis hash 是一个键值(key=>value)对集合。

List(列表)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

Set(集合)

Redis的Set是string类型的无序集合。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

zset(sorted set:有序集合)

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

SpringBoot操作Redis数据

SpringBoot操作四大类型的操作实例

Redis事务

MULTI、EXEC、DISCARD和WATCH命令是Redis事务功能的基础。

Redis事务是一种乐观锁的机制,当事务过程中数据对象被改变,事务直接回滚

在这里插入图片描述

过程:用watch监控键值对,开启事务,判断键值对没被改变,那成功修改;如果判断键值对被修改,回滚事务

乐观锁ABA问题:提交事务前,监控事务的过程中,其他连接对该数据进行操作,比如+500,然后又改回来 -500,这种情况,如果以数据变化进行判断是否有无操作,此处将会判别为未被改动,但实际数据是动过的,这时事务会提交成功,就是非安全操作;

redis不会有该aba问题的原因就在于其是通过版本比对,操作数据一次就+1,事务前后版本不同则判别非安全操作,事务不会提交

Redis在Java中的事务操作

由于使用redisTemplate的exec不在同一个连接之中,事务无法提交.所以redis事务应该在execute的回调方法中执行(保证同一连接)

@Test
    public void transactionTest() {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                redisOperations.watch("mn");
                redisOperations.multi();
                redisOperations.opsForValue().get("mn");
                redisOperations.opsForValue().set("mn",10000);
                //以下exec是以上操作的结果集
                List exec = redisOperations.exec();
                System.out.println(exec);
                return null;
            }
        });
    }

注意redis事务不会因为数据类型转换的异常而回滚,比如set name “xiaoming” ,在事务提交之前进行自增,这时候事务不会回滚

Redis流水线

类似于队列批量协议,因为网络原因,redis单条查询速度远小于性能,所以可以一次发送一组操作命令,加快执行速度

正常速度:
 @Test
    public void setTest() {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                long preTime = System.currentTimeMillis();
                int i = 0;
                while (true) {
                    long newTime = System.currentTimeMillis();
                    if (newTime - preTime > 1000) {
                        break;
                    }
                    redisTemplate.opsForValue().set("name", i++);
                }
                //1371次
                System.out.println(i+"次");
                return null;
            }
        });

    }	
pipeLined速度
@Test
    public void executePipelined() {
        redisTemplate.executePipelined(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                long preTime = System.currentTimeMillis();
                int i = 0;
                while (true) {
                    long newTime = System.currentTimeMillis();
                    if (newTime - preTime > 1000) {
                        break;
                    }
                    redisTemplate.opsForValue().set("name", i++);
                }
                //68721次
                System.out.println(i+"次");
                return null;
            }
        });

    }

Redis发布订阅

开启一个SUBSCRIBE mn A

另一个PUBLISH mn 1000 B

SUBSCRIBE mn
PUBLISH mn 1000

那么在A中可以收到消息

在这里插入图片描述

Redis超时命令

用于防止某些命令执行过程出错,但是一直占用资源的情况,设置超市可以及时回收内存资源

常用超时命令
命令名作用
persist时间永生
expire设置时间*秒
ttl查看剩余时间*秒(-1标识永生,-2标识被移除)
pttl查看剩余时间*毫秒(-1标识永生,-2标识被移除)
redisTemplate.expire("name", 25, TimeUnit.SECONDS);
Redis回收注意

key超时后,仍然不会从内存中移除

  1. 当调用get xxx时一定会移除超时的key
  2. 程序可编写一个定时器,定时移除超时的key
  3. 内存满的时候,再加入新的数据时,根据内存淘汰策略决定时候移除超时的key

redis.conf

持久化配置

快照RDB

将redis的内存结构制作快照(save命令),再进入redis的是恢复快照*默认开启

  1. save 900 1
    save 300 10
    save 60 10000
    

    分别表示900秒内有一个写命令备份快照;表示300秒内有10个写命令备份;表示60秒内有10000个写命令备份

  2. stop-writes-on-bgsave-error yes

    后台保存出错的时候停止写入操作

  3. dbfilename dump.rdb

    实际上就是redis的数据快照文件,这个文件如果转移到其他虚拟机,那那台虚拟机就有了被转移目标的redis数据

  4. rdbchecksum yes

    是否对快照文件进行验证

AOF

AOF值保存增删改命令:类似于mysql的数据日志,只将执行过的写命令依次保存在redis文件中,以后依次执行这些命令即可恢复redis数据*因为每次操作都有文件的读写命令,默认不开启

  1. appendonly no

    是否启用aof备份,需要就改为yes*默认为no

  2. appendonly.aof

    redis数据通过aof持久化的文件

  3. appendfsync

    持久化策略

    • always 每次操作
    • everysec 美妙
    • no 一般情况*默认 (不影响性能)
  4. auto-aof-rewrite-percentage 100

    指定aof重写文件的条件,100表示文件增长量大于之前100%,则重写,配置为0表示禁用

    64M表示文件达到64M,则重新写入新文件

在这里插入图片描述

  1. aof-load-truncated yes

    redis恢复文件是否忽略报错语句*默认yes

内存回收策略

lru:lru和ttl都是不精准的算法,如图,先从所有的key,随机找三个放左边进行查看使用次数少的,然后淘汰它,单这种取样本淘汰的可能不是所有key里面使用最少的,但是如果要找出所有key使用最少,太耗费性能

在这里插入图片描述

  1. volatitle-lru 设置时间的按使用少的淘汰
  2. allkeys-lru 所有key按使用少的淘汰
  3. volatitle-random 随机淘汰设置时间的
  4. allkeys-random 随机淘汰所有的*不建议
  5. volatitle-ttl 删除存活时间最短的键值对
  6. noevition 默认

lua脚本

redis要求2.6以上,lua类似于js,是一个脚本语言,C语言编写,嵌入到redis进行业务逻辑的处理,也可用于nginx

redis是单线程

redis本身就是内存操作,对处理速度已经很快了,多线程模式由于其切换上下文的时间成本,不利于redis的执行速度.单线程的特点使得其执行lua的时候是不会打断的,所以是原子性操作(redis操作要么全部提交要么全部不提交)的

LUA语言基础

CenterOS内置lua

查看版本,有版本号就表示有

lua

基本数据类型

类型解释
nil相当于null
booleantrue/false
number整型+浮点型
string字符型(三种表达方式"";’’;[[]] )

String

  1. 字符串赋值

    > a = "String1" 
    > b = 'String2'
    > c = [[String3]]
    > print a
    stdin:1: '=' expected near 'a'
    > print (a)
    String1
    > print (b)
    String2
    > print (c)
    String3
    >
    
  2. 字符串拼接

    a = "a" .. "b"
    

LUA中的方法

a = 20
b = 30
function test()
local a = 10
local b = 30
print("a="..a.." b="..b)
end
test()
print ("a="..a.." b="..b)

执行得到

a=10 b=30
a=20 b=30

因为后一个print (“a=”…a…" b="…b)得到的是最外面的全局a b,第一个print得到的是本地的a 和 b

基本表达式

条件表达式
age = arg[1]
if(tonumber(age) < 18) then
        print("未成年")
   else
        if(tonumber(age)<60) then
                print("中年")
        else
                 print("老年")
        end
end
year = arg[1]

if(((year%4==0)and(year%100~=0))or(year%400==0)) then
        print("is闰年")
else
        print("isnot闰年")
end
数组
array={1,3,5,7,9}
for i=1,#array,1 do
 print("数组的值是"..array[i])
end
while循环
i = 0

while(i<10) do
        print("i="..i)
        i=i+1
end
for循环
for i=0,9,1 do
        print("i的值是"..i)
end
冒泡排序
arr={1,2,55,21,34,14,12,51,6}

for i=1,#arr-1,1 do
        for j=1,#arr-i,1 do
                if(arr[j]>arr[j+1]) then
                        arr[j],arr[j+1] = arr[j+1],arr[j]
                end

        end
end

for i=1,#arr,1 do
        print(arr[i])
end

Redis使用lua表达式

通过传过来lua脚本文件给redis,给lua执行,lua脚本可以写在配置文件

返回字符

eval "return 'hello world!'" 0

0标识这个脚本有几个key

给name赋值

eval "redis.call('set','name','xiaoming')" 0

取name值

 EVAL "return redis.call('get','name')" 0

给hashmap赋值person.name = xiaoming 类似于hset person name xiaoming

eval "redis.call('hset',KEYS[1],ARGV[1],ARGV[2])" 1 person name xiaoming

注意参数名要大写

lua脚本的缓存

有时候为了反复执行同一段脚本,但是脚本内容可能很长,那么传输过程会很耗费性能.缓存可以解决这问题.缓存后,redis使用sha-1签名算法加密脚本,返回一个判别缓存脚本的字符串

  1. 进入./redis.cli,保存脚本进缓存

    script load "redis.call('set',KEYS[1],ARGV[1])"	
    

    得到"7cfb4342127e7ab3d63ac05e0d3615fd50b45b06"的使用字符串

  2. evalsha调用启动用的脚本字符串

    EVALSHA "7cfb4342127e7ab3d63ac05e0d3615fd50b45b06" 1 name hu
    

在这里插入图片描述

Java执行lua脚本

运行使用普通lua脚本
 @Test
    public void evalScript() {
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        String luaStr = "redis.call('set',KEYS[1],ARGV[1])";
        //lua方式不会序列化key value
//        connection.eval(luaStr.getBytes(), ReturnType.VALUE, 1, "dog".getBytes(), "cat".getBytes());
        String luaGetStr = "return redis.call('get',KEYS[1])";
        byte[] eval = connection.eval(luaGetStr.getBytes(), ReturnType.VALUE, 1, "dog".getBytes());
        System.out.println(new String(eval));
    }
运行使用普通lua脚本缓存lua脚本
  @Test
    public void evalScriptChe() {
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        String luaStr = "redis.call('set',KEYS[1],ARGV[1])";
        String returnStr = connection.scriptLoad(luaStr.getBytes());
        //7cfb4342127e7ab3d63ac05e0d3615fd50b45b06
        System.out.println(returnStr);
    }
通过缓存脚本返回的身份字符串复调lua脚本
 @Test
    public void useSriptChe() {
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//        String luaCheStr = "7cfb4342127e7ab3d63ac05e0d3615fd50b45b06";
//        connection.evalSha(luaCheStr, ReturnType.VALUE, 1, "dog".getBytes(), "cat".getBytes());
        String luaStr = "return redis.call('get',KEYS[1])";
        byte[] result = connection.eval(luaStr.getBytes(), ReturnType.VALUE, 1, "dog".getBytes());
        System.out.println(new String(result));
        System.out.println(redisTemplate.opsForValue().get("dog"));
    }
以文件形式运行lua文件
 @Test
    public void runLuaWithFile() throws IOException {
        //获取classes编译目录
        String path = this.getClass().getResource("/").getPath();
        System.out.println(path);
        String decodePath = URLDecoder.decode(path);
        String luaPath = decodePath + "luaScript.lua";
        File file = new File(luaPath);
        if (file.exists()) {
            System.out.println("文件存在");
        }

        //将脚本文件转化成byte数组
        FileInputStream fileInputStream = new FileInputStream(file);
        ByteOutputStream byteOutputStream = new ByteOutputStream();
        int len;
        byte[] bytes = new byte[1024 * 10];
        while ((len = fileInputStream.read(bytes)) != -1) {
            byteOutputStream.write(bytes, 0, len);
        }
        byte[] byteArray = byteOutputStream.toByteArray();
        byteOutputStream.close();
        //获取redis连接
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();

        //执行脚本
        byte[] eval = connection.eval(byteArray, ReturnType.VALUE, 1, "dog".getBytes());
        System.out.println(new String(eval));
    }

Redis缓存服务器

 @RequestMapping("/getAll")
    @ResponseBody
    public Object getAll(int id) {
        String key = "user" + id;
        User user = (User) redisTemplate.opsForValue().get(key);

        if (user != null) {
            user = userDao.selectByPrimaryKey(id);
            //重建该缓存
            redisTemplate.opsForValue().set(key,user);
            redisTemplate.expire(key, 5, TimeUnit.MINUTES);
        }
        return user;

    }
缓存失效的特殊情况
  • 某一时刻多个缓存失效,重建缓存

    解决办法,超时时间错开

  • 某一时刻,某个热门缓存失效,这时候大量数据进入if (user != null) {}代码中,都要重建缓存

    解决办法:加锁,分布式锁,使用redis的setnx key value指令

在这里插入图片描述

SpringBoot管理Redis缓存

注解意义
@Cacheable(key = “‘user’”,value = “cache”)表示redis服务器有缓存,就会直接当返回值返回,而不是进入方法*用于查询
@CachePut(key = “‘user’+#id”,value = “cache”)表示每次更新会更新对应的user+i缓存,用于新增和更新
@CacheEvict(key = “‘user’+#id”, value = “cache”)表示每次操作会删除user+i的缓存,用于删除
//查询全部
@Cacheable(key = "'user'",value = "cache")
@Override
public List<User> selectByExample(UserExample example) {
    System.out.println("查询了数据库");
    return userDao.selectByExample(example);
}

//查询单个
@Cacheable(key = "'user'+#id", value = "cache")
@Override
public User selectByPrimaryKey(Integer id) {
    System.out.println("查询了数据库");
    return userDao.selectByPrimaryKey(id);
}
/**CachePut一定会添加到缓存中,而且将返回结果放到缓存中,多用于添加和修改的方法
     * @param record
     * @return
     */
    @CachePut(key = "'user'+#id", value = "cache")
    @Override
    public int updateByPrimaryKey(User record) {
        System.out.println("查询了数据库");
        return userDao.updateByPrimaryKeySelective(record);
    }
@CacheEvict(key = "'user'+#id", value = "cache")
@Override
public int deleteByPrimaryKey(Integer id) {
    System.out.println("查询了数据库");
    return userDao.deleteByPrimaryKey(id);
}
    

/**更新一定要删除集合缓存,同时更新该条数据
* @param record
* @return
*/
@CachePut(key = "'user'+#id",value = "cache")
@CacheEvict(key = "'user'", value = "cache")
@Override
public int insert(User record) {
    System.out.println("查询了数据库");
    return userDao.insert(record);
}

redis集群和集群高可用

主从复制

  1. 主服务启动后,开始bgsave,同时发送同步命令给从服务器,从服务器不会拒绝写操作,会把命令存入缓冲区,根据数据正常返回还是报错
  2. 主服务bgsave完毕,发送数据快照给从,从必须舍弃当前数据开始载入快照
  3. 主服务将之前缓冲区残余的写命令发给从,从完成快照还原,继续完成写,于是主从复制完成

过程:

  • 将所有从服务器的redis.conf下修改slaveof主服务器地址

    slaveof 192.168.9.128 6379
    
  • 如果安装的主服务的redis有设置protect-mode yes,就要设置

    masterauth root
    
    
  • 主服务器,查看集群状态

    info replication
    

在这里插入图片描述

在这里插入图片描述

切换到另一台服务,查找刚才的key

在这里插入图片描述

有两台从服务检测到,配置成功

读写分离

一般项目中,读90%,10%写入,把读单独一台,读9台,redis读写分离基于主从复制,由于redis的从服务网不具写入权限,所有的写入操作均由主服务完成,这就是redis读写分离

在这里插入图片描述

哨兵模式

1 、不时地监控redis是否按照预期良好地运行;

2、如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);

3、能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。

4、哨兵为客户端提供服务发现,客户端链接哨兵,哨兵提供当前master的地址然后提供服务,如果出现切换,也就是master挂了,哨兵会提供客户端一个新地址。

在这里插入图片描述

在高可用环境下,还可以设置哨兵高可用集群(多哨兵模式),多个哨兵互相通信,当多个哨兵侦查到redis服务主机宕机,哨兵之间会进行投票,选取出一台从服务从任主机,同时用发布订阅信息到每一台redis从机,修改他们的slaveof ip+端口

配置哨兵
  1. 如果安装的主服务的redis有设置protect-mode no,就要设置,取消protected-mode no的注释

    protected-mode no
    
  2. 默认端口26379

  3. 设置默认主机ip

    sentinel monitor mymaster 192.168.9.128 6379 2
    

    2代表只要有2个或者2个以上的哨兵认为主机宕机则选举从机为主机

  4. 如果设置了主机密码,下面就要把注释去掉,并设置连接名和密码

    # sentinel auth-pass <master-name> <password>
    
  5. 启动redis哨兵

    ./redis-sentinel sentinel.conf
    
  6. 分别为其他服务器也配置哨兵,方法如上,并分别启动,启动之前务必将26379端口开启

  7. sentinel down-after-millseconds设置哨兵检测redis服务.在这个时间内没有应答任务主观下线,默认30秒

  8. sentinel failover-timeout指定故障切换的时间*毫秒,当超过这个时间则认定为故障,默认3分钟

  9. sentinel notification-script发生redis异常触发的脚本

集群测试

启动原有的集群情况下,新建一个128的连接进入主服务的redis-cli,shutdown其服务

在这里插入图片描述

  • sdown:当前sentinel实例认为某个redis服务为"不可用"状态. 表示当前128主机宕机
  • vote-for-leader:表示开始投票新的redis主机
  • odown表示多个sentinel实例都认为master处于"SDOWN"状态,那么此时master将处于ODOWN,ODOWN可以简单理解为master已经被集群确定为"不可用",将会开启failover.
  • failover:表示确认为失效待补救状态(上面翻译是:我会在昨天22点之后搞定问题)
  • switch-master:转换主机到192.168.9.129
这时候查看130(第二台从服务)的redis.conf

在这里插入图片描述

选举之后还是从服务

再查看129的redis.conf

在这里插入图片描述

没有slaveof

再看129的redis-cli的replication

在这里插入图片描述

role是master

说明哨兵在选举之后,改变了redis.conf这个配置文件

如果这时候重启128

在这里插入图片描述

前主机再宕机后,即便重启,它的皇帝宝座也不再是它的了,最终还是老老实实做回老从吧

这样一来,所有的新增操作,都是由主服务操作,读则可以是redis主服务,也可以是从服务达到读写分离

Cluster模式分片集群

通过一个对key的hash函数,对应到的一个对16384取余,获得一个0到16383的值,在通过这个值,确定在哪个redis服务上执行

在这里插入图片描述

单点登录

分布式项目中,在登录工程进行登录之后,怎么在其他工程也保持登录状态,我们知道session是可以在同一台tomcat中共享,但是在多个项目之间可以用redis进行共享,通过登录成功把随机的uuid作为key,user对象作为value放在redis中,再取名一个字符串"token"做为key,uuid做为value防止在cookie中

我这里用的是dubbo分布式架构构建

搭建步骤

1.登录进行验证,验证成功生成token并保存redis和cookie
 @RequestMapping("/login")
    public Object login(User user, HttpServletResponse response) {
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andUsernameEqualTo(user.getUsername());
        User userRS = userService.selectByExample(userExample).stream().findFirst().
            orElse(null);
        if (userRS.getPassword().equals(user.getPassword())) {
            String uuid = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(uuid,user);
            redisTemplate.expire(uuid, 30, TimeUnit.MINUTES);
            Cookie cookie = new Cookie("token",uuid);
            //7天有效
            cookie.setMaxAge(7 * 24 * 60 * 60);
            //设置host
            cookie.setDomain("localhost");
            /*可能会有跨域的问题,为了防止银行的cookie被其他地址使用,所有本项目的cookie才
           能本项目使用,所以要设置setPath("/"); */
            cookie.setPath("/");
            response.addCookie(cookie);
        }
        return "redirect:http://localhost:8084/userRedis/";
    }

Cookie设置的过程一定要指定路径为"/".因为存在跨域问题

2.在其他工程首页验证cookie
@RequestMapping("getCookie")
@ResponseBody
public Object getCookie(@CookieValue("token") String token) {
    return token;
}

CookieValue等同于

Cookie[] cookies = request.getCookies();
    for (Cookie cookie : cookies) { 
    if (cookie.getName().equals("token")) {
    	String value = cookie.getValue(); 
        break; 
    } 
} 

required = false表示该参数并不是必须,没有传值的时候为null

跨域问题

1.JS跨域

当不同工程互相ajax访问,比如8084端口访问8085端口的资源,就会发生不同域请求禁止访问的问题,使用jsonp原理:我们知道html加载其他网站的js,是不会发生跨域问题,jsonp相当于把你要访问的内容*(json数据),以加载网络js的方式下载过来,设置回调callback方法可以进行处理

在这里插入图片描述

$(function () {
    $.ajax({
        url:"http://localhost:8085/login/isLogin",
        dataType:"jsonp",
    });
})

function callback(data) {
    if (data != "") {
        $("#usernameLB").html(data.username+
		"<a href='http://localhost:8085/login/logout'>注销</a>");
    }
}
2.Cookie跨域

一般情况下,如果不对cookie设置path的话,默认按照当前设置cookie的项目路径当做了path,为了安全(比如淘宝的cookie不可能让京东去使用),因此其他path的工程就无权使用此cookie,设置跨域如下

cookie.setPath("/")

登录返回登录前页面

思路:把登录的时候前的地址当参数传递给后台

<a href="javascript:login()">登录</a>
function login() {
    var url = location.href;
    //用于对汉字进行转成unicode,避免乱码
    encodeURI.replace("&", "*");
    var encodeURI = encodeURI(url);
    location.href = "http://localhost:8084/userRedis/lgin?lookUrl="+encodeURI;
}
 @RequestMapping("/lgin")
    public Object lgin(Model model,String lookUrl) {
        model.addAttribute("lookUrl", lookUrl);
        return "Login";
    }
 <form action="http://localhost:8085/login/login" method="get">
        <input type="hidden" name="lookUrl" th:value="${lookUrl}"/>
        账号<input type="text" name="username"></br>
        密码<input type="text" name="password">
        <input type="submit" value="提交">
    </form>
@RequestMapping("/login")
    public Object login(User user, HttpServletResponse response,String lookUrl) {
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andUsernameEqualTo(user.getUsername());
        User userRS = userService.selectByExample(userExample).stream().findFirst().orElse(null);
        if (userRS.getPassword().equals(user.getPassword())) {
            String uuid = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(uuid,user);
            redisTemplate.expire(uuid, 30, TimeUnit.MINUTES);
            Cookie cookie = new Cookie("token",uuid);
            //7天有效
            cookie.setMaxAge(7 * 24 * 60 * 60);
            //设置host
            cookie.setDomain("localhost");
            //可能会有跨域的问题,为了防止银行的cookie被其他地址使用,所有本项目的cookie才能本项目使用,所以要设置setPath("/");
            cookie.setPath("http://localhost:8084/userRedis/");
            response.addCookie(cookie);

        }
        //前端对url进行&转*,后台转回来,防止在login方法开始的时候就把http://localhost:8085/login/login?lookUrl = http://localhost:8084/userRedis/?id=1&name=2
        //中的lookUrl = http://localhost:8084/userRedis/?id=1当成一个参数name当做第二个参数
        lookUrl = lookUrl.replace("*", "&");
        System.out.println(lookUrl);
        if (lookUrl == null) {
            lookUrl = "";
        }
        return "redirect:"+lookUrl;
    }

购物车应用

一般购物车只需要放置商品id,数量和用户id,订单里面除了有这些字段,还需要加入购买时的商品图片,商品价格.商品详情.因为商品时刻在变,下单的价格则只需要一个时刻.

  • session缺点:浏览器关闭就没了

  • cookie:pc端的购物车无法在移动端

  • 数据库:让数据库压力太大

  • redis:和cookie类似

淘宝购物车:淘宝的购物车就是登录才能添加,

京东的购物车:不需要登录就可以添加. cookie+数据库,不使用redis的原因是有些人喜欢在购物车添加大量商品,这样无端占用了redis的内存空间

秒杀

倒计时

为保证秒杀业务的过程,必须用服务器时间进行参照,可以配置几台时间服务器,专门用来访问获取实际时间,为保证几台服务器的时间同步,要求每过一段时间对官方时间服务进行同步

库存减少策略
  1. 点击抢购即可减库存
  2. 付完款才减库存
假设1万人抢5000的库存
public synchronized void addOrder() {
   for (int i = 0; i < 10000; i++) {
       new Thread() {
           @Override
           public void run() {
               Order order = orderService.selectByPrimaryKey(14);
               Integer count = order.getCount();
               if (count > 0) {
                   order.setCount(count - 1);
                   orderService.updateByPrimaryKey(order);
                   Miaosha record = new Miaosha();
                   miaoshaService.insert(record);
               }
           }
       }.start();
   }
}

不带任何锁 17秒

却产生了7809个订单,超卖

在这里插入图片描述

给程序加同步锁 2分05秒

public synchronized void addOrder() {}

然而执行时间却变得很长

使用悲观锁(for update) 1分35秒 数据可靠

悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作

在这里插入图片描述

排它锁只能 作用在唯一主键或者唯一索引,不然就是表锁
SELECT * FROM `order` where id = 1 for update

排它锁:执行增删改,自带排他锁,如果在select语句后加for update,那么其他线程也不能操作该记录,在悲观锁内,锁住的记录,其他操作(除了select)必须等他事务提交才能继续操作

排它锁行锁:当forupdate作用在为唯一列,则只锁住操作行,其他行查询不受影响

使用乐观锁 29秒 没卖完

乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。

没卖完的原因是:查询数据库发现有人在用,那我不用了,导致商品没卖完,解决办法写个循环卖剩下的

首先在数据库表添加version字段
update order set count=count+1,version=version+1 where id = 1 and version=#{version}
直接减库存 33秒 没有超卖,也卖完了
不查询直接减库存,在sql上就做判断
update order set count=count+1 where id = 1 and count >= 1

redis实现秒杀

利用redis访问速度快的优势,再大并发到来的时候,把商品的数量数据准备在redis中,通过lua脚本的原子性对用户的采购暂时保存在redis中,当秒杀结束的时候,同步数据到sql数据库

  1. 编写lua脚本,要让每次购买,对应商品-1,同时保存秒杀商品整个对象

    local num = tonumber(ARGV[1])
    local id = KEYS[1]
    local count = tonumber(redis.call('hget','order'..id,'count'))
    if count == nil or count < num then
        return 1
    end
    
    count = count - num
    redis.call('hset','order'..id,'count',count)
    
    local order = ARGV[2]
    redis.call('rpush','miaosha'..id,order)
    
    if count == 0 then
        return 2
    else
        return 1
    end
    
  2. 初始化定义为对象序列化为String类型

    @PostConstruct
    public void init() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
    }
    
  3. 执行lua脚本(这里省略了lua文件的读取)

    for (int i = 0; i < 2000; i++) {
                final int a = i;
                new Thread() {
                    @Override
                    public void run() {
                        order.setCount(1);
                        order.setGoodid(a);
                        String miaoshaObject = new Gson().toJson(order);
                        //integer代表返回值类型是一个数字
                        long  result = connection.evalSha(luaCache, 
    								ReturnType.INTEGER, 1, (1 + "").getBytes(),
    								(1 + "").getBytes(), miaoshaObject.getBytes());
                        if (result == 2) {
                            System.out.println("商品秒杀结束,开始同步数据库!");
                            asyncService.Async();
                        }
                        connection.close();
    
                    }
                }.start();
            }
        }
    
  4. 使用springBoot的方法异步注解,使得方法异步执行,同步数据库,商品添加到数据库

    使用方法,现在启动类上标志@EnableAsync,后标注要异步的方法上@Async

    /**
    * 同步调用
    */
    @Override
    @Async
    @Transactional
    public void Async() {
        List<String> miaoshaList = redisTemplate.opsForList().range("miaosha1", 0, -1);
        for (String miaoshaStr : miaoshaList) {
            Gson gson = new Gson();
            TypeToken<Miaosha> typeToken = new TypeToken<Miaosha>(){};
            Miaosha miaosha = gson.fromJson(miaoshaStr, typeToken.getType());
            miaoshaDao.insert(miaosha);
        }
    
        System.out.println("------------------同步完成------------------");
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值