redis基础知识随记

1 Redis概念

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

2 Redis基本知识

Redis采用单线程机制进行工作。
Redis的默认端口是6379。
keys * 查看所有的key (匹配:keys *1)
set key1 value1 设置key
get key 获取key的value
type key 查看key的类型
exists key 判断某个key是否存在
del key 删除指定的key数据
unlink key 根据value选择非阻塞删除,仅将keys从keysapce元数据中删除,真正的删除会在后续异步操作。
expire key 10 为给定的key设置超时时间为10秒
ttl key 查看还有多久过期,-1表示永不过期,-2表示已经过期
select命令切换数据库,15个数据库,默认使用0号数据库。
dbsize 查看当前数据库的key的数量
flushdb 清空当前库

3 基本数据类型

3.1 String

String是redis最基本的数据类型,字符串value最多可以是512M。

3.1.1 数据结构

String类型的数据结构为简单动态字符串(Simple Dynamic String,SDS),是可以修改的字符串,内部结构实现类似与ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。不够用时会扩容,加倍现有的空间。

3.1.2 基本命令

append key value 追加一个值到key上。
strlen key 获得值的长度。
setnx key value 只有key不存在时 才设置key的值。
incr key 将key中存储的数值增1,只能对数字值操作,如果为空,新增值为1。原子性的操作,因为redis的单线程。java里的i++是非原子性的。
decr key 将key中存储的数值减1。
incrby/decrby key val 将key中存储的数值增减,自定义步长。
mset k1 v1 k2 v2 k3 v3 设置多个key值。
msetnx 设置多个key的值,当且仅当所有给定key都不存在时才会成功。
getrange key len1 len2 获取起始位置到结束位置的value值。
setex key time value 在设置值时设定过期时间。
getset key value 设置了新值同时获取旧值。

3.2 List

单键多值,简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或者尾部,底层是双向链表,对两端的操作性能很高,通过索引下标操作中间元素性能会较差。

3.2.1 数据结构

List的数据结构为quickList,首先在列表元素比较少的情况下会使用一块连续的内存存储,这块内存是ziplist(压缩列表)。当数据量比较多的时候才会改成quickList,也就是将多个ziplist用双向指针串起来组成quickList。因为普通链表需要附件的指针空间太大,比较浪费空间。

3.2.2 基本命令

lpush/rpush k1 v1 v2 v3 从左/右边开始插入一个或者多个值
lrange key begin end 根据索引下标获取元素。 lrange 0 -1 取出所有的元素。
lpop/rpop 从左边/右边吐出一个值,值在键在,值光键亡。
rpoplpush key1 key2 从key1列表右边吐出一个值,插到key2列表的左边。
len key 获取列表长度。
lindex key index 按照索引下标获取元素(从左到右)。
linsert key before value newvalue 在value的值前面插入newvalue值。
lrem key n value 从左边删除n个value。
lset key index value 将列表下标为index的值替换为value。

3.3 Set

Set是String类型的无序集合,它底层是一个value为null的hash表,所以添加、删除、查找的复杂度都是O(1)。

3.3.1 数据结构

Set数据结构是dict字典,字典使用hash表实现的。

3.3.2 基本命令

sadd key v1 v2 v3 添加一个或者多个元素到集合里。
smembers key 取出该集合所有的值。
sismember key value 判断集合key是否包含该值,有返回1,无返回0。
srem key v1 v2 … 删除集合里的某些元素。
spop key 随机吐出一个值。
srandmember key n 随机从该集合中取出n个值,不会从集合中删除。
smove src dest value 把集合中的一个值移到另一个集合。
sinter k1 k2 返回2个集合的交集元素。
sunion k1 k2 返回2个集合的并集元素。
sdiff k1 k2 返回2个集合的差集。(k1中的,不包含k2中的)。

3.4 Hash

Hash是一个String类型的field和value的映射表,hash特别适合存储对象,类似于java里的Map<String,Object> 。

3.4.1 数据结构

Hash类型的数据结构有2种:ziplist(压缩列表)和HashTable(哈希表),当field-value长度较短且个数较少时,使用ziplist,否则使用hashTable。

3.4.2 基本命令

hset key field value 给哈希表key中的field键赋值value。
hsetnx key field value 给哈希表key中的field键赋值value,仅当field不存在时。
hget key field 从集合key的field取出值。
hmset key f1 v1 f2 v2… 批量设置hash的值。
hexists key field 查看hash表中给定的field是否存在。
hkeys key 列出该hash集合的所有field。
hvals key 列出该hash集合的所有value。

3.5 Zset

有序集合Zset与Set非常相似,是一个没有重复元素的字符串集合。不同之处在于有序集合的每个成员关联了一个score评分,这个评分被用来按照从低到高的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。

3.5.1 数据结构

Zset的数据结构,可以认为等价于java的Map<String,Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似TreeSet,内部会按照score排序,可以通过score的范围来获取元素的列表。
zset底层用了2种数据结构:
1 hash,hash的作用是关联value跟score,保障元素value的唯一性,可以通过元素value找到score。
2 跳跃表,目的在于给元素value排序,根据score的范围获取列表。

3.5.2 基本命令

zadd key score1 v1 score2 v2 … 将一个或多个元素及其score值加入到有序集合key中。
zrange key start stop withscores 返回下标在start和stop之间的元素,加上withscores可以将评分跟值一起返回到结果集。

4 发布与订阅

实现步骤:
1 打开一个客户端订阅channel1。
subscribe channel1
2 打开另一个客户端,给channel1发布消息hello。
publish channel1 hello ,会返回订阅者数量
3 打开第一个客户端会看到发送的消息。

5 Redis测试案例

conf配置文件修改 protected-mode 为no,注掉bind,不然只能本机访问。
linux 可以产查看火墙状态的命令:systemctl status firewalld
关闭防火墙:systemctl stop firewalld
模拟发送短信验证码,每个手机每天最多发送3次,验证码有效期2分钟。

public class SmsCode {

    public static void main(String[] args) {
        codeSend("15211112222");
//        verifyCode("15211112222","843428");
    }

    public static boolean verifyCode(String phone,String code){
        boolean flag = false;
        Jedis jedis = new Jedis("127.0.0.1",6379);
        String codeKey = "verifyCode"+phone+"code";
        String redisCode = jedis.get(codeKey);
        if(code.equals(redisCode)){
            flag = true;
            System.out.println("验证成功");
        }
        jedis.close();
        return flag;
    }

    public  static String codeSend(String phone){
        String code="";
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //设置验证码的key
        String codeKey = "verifyCode"+phone+"code";
        //设置验证码发送次数key
        String countKey = "verifyCode"+phone+"count";
        String count = jedis.get(countKey);
        if(count == null){
            //第一次发送
            jedis.setex(countKey,60*60*24,"1");
            System.out.println(phone+"第一次发送");
        }else if(Integer.parseInt(count) <=2){
            jedis.incr(countKey);
        }else if(Integer.parseInt(count)>=3){
            System.out.println("今天发送次数已经超过3次");
            return "";
        }
        //将code放入redis
        code = getCode();
        jedis.setex(codeKey,120,code);
        jedis.close();
        return code;
    }

    public static String getCode(){
        String code = "";
        Random ran = new Random();
        for (int i = 0; i <6 ; i++) {
            int r = ran.nextInt(10);
            code = code + r;
        }
        System.out.println(code);
        return code;
    }
}

6 Redis的事务和锁机制

Redis事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。

6.1 multi、exec、discard

从输入multi命令开始,输入的命令都会依次进入到命令队列中,但不会执行,直到输入exec后,redis才会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。

6.2 事务的错误处理

1 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
2 如果执行阶段某个命令出现了错误,那么只有报错的命令不会被执行,其他的命令都会执行,不会回滚。

6.3 悲观锁与乐观锁

悲观锁:每次去拿数据时都会认为别人会修改,所以每次拿数据时都会上锁,这样别人想拿到这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,例如行锁、表锁等,都是在做操作前上锁。
乐观锁:每次去拿数据时都认为别人不会修改,所以不上锁,但是在更新的时候会判断一下在此期间别人有没有更新过这个数据,可以使用版本号等机制,乐观锁适用于多读的类型,这样可以提高吞吐量。Redis就是利用这种Check-and-set机制实现的。

6.4 watch

在执行multi之前,先执行watch key1 key2… 可以监视一个或者多个key,如果在事务执行之前,这些key被其他命令所改动,那么事务会被打断。
例如开启2个客户端,都watch balance
client1:
watch balance
multi
incrby balance 10
exec

client2:
watch balance
multi
incrby balance 20
exec
当客户端1提交了事务显示修改成功,此时去提交客户端2的事务就会报错了。因为乐观锁。
unwatch key1 key2…
取消watch命令对key的监视。

6.5 事务三个特性

1 单独的隔离操作
事务中的所有命令都会序列化、按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
2 没有隔离级别的概念
队列中的命令没有提交之前都不会被实际执行,因为事务提交前任何指令都不会被实际执行。
3 不保证原子性
事务中如果有一条命令执行失败,其他的命令仍然会被执行,没有回滚。

7 SpringBoot整合redis

7.1 引入依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

7.2 配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport{

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer 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);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer 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);
        //配置序列化(解决乱码的问题),过期时间为600秒
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .defaultCacheConfig().entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
        return cacheManager;
    }

}

7.3 使用案例

@RestController()
@RequestMapping("/redis")
public class RedisController {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisService redisService;

    @GetMapping("/test")
    public String testRedis() {
        String name= "";
            redisTemplate.opsForValue().set("name","neo");
            name = (String)  redisTemplate.opsForValue().get("name");
        return name;
    }
}

8 秒杀案例

8.1 使用连接池,使用事务解决超时跟超卖的问题。

业务实现层:

@Service
public class RedisServiceImpl implements RedisService {

    /**
     * 高并发下出现超卖跟连接超时问题
     * @param userid 用户id
     * @param pid 商品id
     * @return
     */
    @Override
    public String secKill(String userid, String pid) {
        if(userid == null || pid == null){
            return "用户或商品不能为空";
        }
        //连接redis
//        Jedis jedis = new Jedis("127.0.0.1",6379);
        //使用连接池工具,解决连接超时问题。
        JedisPool jedisPool = JedisPoolsUtil.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();

        //库存key
        String kcKey = "sk:"+pid+":qt";
        //秒杀成功用户key
        String userKey = "sk:"+pid+":user";

        //监视库存
        jedis.watch(kcKey);

        //获取库存
        String kc = jedis.get(kcKey);
        if(kc == null){
            jedis.close();
            return "秒杀还未开始";
        }

        //判断用户是否秒杀成功过
        if(jedis.sismember(userKey,userid)){
            jedis.close();
            return "秒杀已经成功,不可重复操作!";
        }

        //判断库存数量
        if(Integer.valueOf(kc)<=0){
            jedis.close();
            return "秒杀已经结束了";
        }

       /* //秒杀过程,库存减1
        jedis.decr(kcKey);
        //添加秒杀成功用户
        jedis.sadd(userKey,userid);
        jedis.close();*/

       //对于超卖问题,可以使用事务
        Transaction multi = jedis.multi();
        //组队操作
        multi.decr(kcKey);
        multi.sadd(userKey,userid);
        //执行
        List<Object> res = multi.exec();
        if(res != null && res.size()>0){
            return "秒杀成功";
        }else{
            return "秒杀失败";
        }
    }
}

Redis连接池工具类如下:

public class JedisPoolsUtil {

    private static volatile JedisPool jedisPool = null;

    private JedisPoolsUtil(){
    }

    public static JedisPool getJedisPoolInstance(){
        if(jedisPool == null){
            synchronized (JedisPoolsUtil.class){
                if(jedisPool == null){
                    JedisPoolConfig config = new JedisPoolConfig();
                    config.setMaxTotal(200);
                    config.setMaxIdle(32);
                    config.setMaxWaitMillis(100*1000);
                    config.setBlockWhenExhausted(true);
                    config.setTestOnBorrow(true);//ping PONG
                    jedisPool = new JedisPool(config,"127.0.0.1",6379,60000);
                }
            }
        }
        return jedisPool;
    }
}

8.2 使用Lua脚本解决库存遗留问题

库存遗留问题是由于乐观锁造成的。
Lua是一个小巧的脚本语言,Lua脚本可以很容易被C/C++代码调用,也可以反过来调用C/C++的函数,一个完整的Lua解释器不过200K,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。
很多应用程序、游戏使用Lua作为自己的嵌入式脚本语言,以此来实现可配置、可扩展性。

8.2.1 Lua脚本再Redis中的优势

将复杂的或多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。
Lua脚本类似redis事务,有一定原子性,不会被其他命令插队,可以完成一些redis事务的操作。
只有再Redis2.6的版本上才可使用Lua脚本。

8.2.2 Lua脚本

local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local userkey="sk:"..prodid.."user";
local userExists=redis.call("sismember",userkey,userid);
if tonumber(userExists)==1 then
  return 2;
end
local num=redis.call("get",qtkey);
if tonumber(num)<=0 then
  return 0;
else
  redis.call("desr",qtkey);
  redis.call("sadd",userkey,userid);
end
return 1;
public class SecKill_redisByScript {
    static String secKillScript = "local userid=KEYS[1];\r\n" +
            "local prodid=KEYS[2];\r\n" +
            "local qtkey=\"sk:\"..prodid..\":qt\";\r\n" +
            "local userkey=\"sk:\"..prodid..\"user\";\r\n" +
            "local userExists=redis.call(\"sismember\",userkey,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(\"desr\",qtkey);\r\n" +
            "  redis.call(\"sadd\",userkey,userid);\r\n" +
            "end\r\n" +
            "return 1";

    public static void doSecKill(String userid,String prodid){
        JedisPool jedisPool = JedisPoolsUtil.getJedisPoolInstance();
        Jedis jedis = jedisPool.getResource();

        String sha1 = jedis.scriptLoad(secKillScript);
        Object o = jedis.evalsha(sha1,2,userid,prodid);

        String result = String.valueOf(o);
        if("0".equals(result)){
            System.out.println("已抢空!");
        }else if("1".equals(result)){
            System.out.println("抢购成功!");
        }else if("2".equals(result)){
            System.out.println("该用户已抢过!");
        }else{
            System.out.println("抢购异常!");
        }
        jedis.close();
    }

    public static void main(String[] args) {
        doSecKill("u0001","p0001");
    }

}
  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值